java多线程之线程同步
2016-12-30 18:24
302 查看
线程不同步问题的出现
当处理共享资源的时候,修改数据和读取数据的同时,多线程不同步会出现脏数据,注意脏数据只是数据错处,并不是说代码逻辑有问题,代码本身是没有什么问题的。举个栗子,共享数据为i,i初始值为1,假设线程分为a,b并且线程不同步。
首先假设我们需要做的操作为:
接下来我使用代码作为实例演示一下问题具体所在:
这里最终结果为80057,也就是车票还剩这么多,但是我设置的每个线程都会自动买10000张车票,这里开了两个线程,结果应该为80000才对,原因在于线程不同步。
解决线程同步问题简单的分成5种
使用synchronized修饰符
其他代码不变,在这个方法上加上一个synchronized,说一下原理,首先java每个对象都有一个内置锁,这里的内置锁是这个类(Ticket),有了这个修饰符就代表,只有这个方法不能同时被多个线程调用,只有其中一个调用完之后才可以让其他线程(包括自己)争夺线程。这时候就可以把整个方法看作一个原子,也就具有了原子性。
使用同步代码块,这里还是使用synchronized
还是将那块代码改变成这样,这个和上面差不多,但是值得一说的线程同步是非常消耗资源的,如果要在这两种选择的话尽量使用这种,不要使用上面那种。
使用volatile特殊变量
volatile在上述场是没有用的。因为volatile只能保证可见性,可见性就是一个线程修改了一个数据,另一个线程是可见的,但是他不具有原子性,所以在遇到非原子性操作的时候是没有用的。
使用ReentrantLock类
使用Threadlocal类
结果太长不好贴出,描述一下
最后结果:线程名Thread-0 : 剩余火车票90000
最后结果:线程名Thread-1 : 剩余火车票90000
结果就是每个线程都执行了10000遍,但是数据不会相互交互,
从线程的角度看,每个线程都保持一个对其线程局部变量副本的隐式引用,只要线程是活动的并且 ThreadLocal 实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。
通过ThreadLocal存取的数据,总是与当前线程相关,也就是说,JVM 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。
ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单,在ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本。
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
上面那个栗子不能体现这种用法的好处,接下来我再写一个栗子。
ThreadLocalTest.java
User.java
两个用户数据不会因为线程而发生任何交互,这样就达到了线程安全。
当处理共享资源的时候,修改数据和读取数据的同时,多线程不同步会出现脏数据,注意脏数据只是数据错处,并不是说代码逻辑有问题,代码本身是没有什么问题的。举个栗子,共享数据为i,i初始值为1,假设线程分为a,b并且线程不同步。
首先假设我们需要做的操作为:
int a = i; a++; i = a;上面代码为两个线程需要执行的代码,当a线程执行代码的时候,局部变量a = i的操作的时候a为1,此时假设还没有执行a++操作,线程b获取资源执行代码,此时b线程对应的局部变量a也为1,然后分别执行a++,然后在赋值,两个线程中的i结果都等于2,我们需要的结果应该是3(至少要一个为3),因为两个线程分别执行这个方法,又因为i是共享资源,所以i应该变为3,由于线程不同步出现了脏数据。有人又会提出质疑,直接i++不久结束了吗?但是java代码执行i++,并不是单纯的执行a++ 一步操作即可,底层是分为多步执行的,既然分步就会有先后也会造成刚刚出现的问题。这个就是非原子性造成的结,这里原子性就是最小的操作·,不能分割了的操作。
接下来我使用代码作为实例演示一下问题具体所在:
public class TestThread { public static void main(String[] args) { Ticket ticket = new Ticket(); //创建两个线程子类 Customer s1 = new Customer(ticket); Customer s2 = new Customer(ticket); s1.start(); s2.start(); } } class Ticket{ //车票数量 private int number = 100000; final Object object = new Object(); public int getNumber() { return number; } public void setNumber(int number) { this.number = number; } //买票 public void buyTicket() { if(number > 0){ number--; } } } class Customer extends Thread{ private Ticket ticket; @Override public void run() { for(int i = 0; i < 10000; i++){ ticket.buyTicket(); System.out.println("线程名"+getName()+" : 剩余火车票"+ticket.getNumber()); if(ticket.getNumber() == 0) break; } } public Customer(Ticket ticket) { this.ticket = ticket; } }结果:
线程名Thread-0 : 剩余火车票80064 线程名Thread-0 : 剩余火车票80063 线程名Thread-0 : 剩余火车票80062 线程名Thread-0 : 剩余火车票80061 线程名Thread-0 : 剩余火车票80060 线程名Thread-0 : 剩余火车票80059 线程名Thread-0 : 剩余火车票80058 线程名Thread-0 : 剩余火车票80057
这里最终结果为80057,也就是车票还剩这么多,但是我设置的每个线程都会自动买10000张车票,这里开了两个线程,结果应该为80000才对,原因在于线程不同步。
解决线程同步问题简单的分成5种
使用synchronized修饰符
public synchronized void buyTicket() { if(number > 0){ number--; } }
其他代码不变,在这个方法上加上一个synchronized,说一下原理,首先java每个对象都有一个内置锁,这里的内置锁是这个类(Ticket),有了这个修饰符就代表,只有这个方法不能同时被多个线程调用,只有其中一个调用完之后才可以让其他线程(包括自己)争夺线程。这时候就可以把整个方法看作一个原子,也就具有了原子性。
使用同步代码块,这里还是使用synchronized
public void buyTicket() { synchronized (object) { if(number > 0){ number--; } } }
还是将那块代码改变成这样,这个和上面差不多,但是值得一说的线程同步是非常消耗资源的,如果要在这两种选择的话尽量使用这种,不要使用上面那种。
使用volatile特殊变量
private volatile int number = 100000;//volatile修饰变量
volatile在上述场是没有用的。因为volatile只能保证可见性,可见性就是一个线程修改了一个数据,另一个线程是可见的,但是他不具有原子性,所以在遇到非原子性操作的时候是没有用的。
使用ReentrantLock类
Lock lock = new ReentrantLock(); public void buyTicket() { lock.lock(); try { if(number > 0){ number--; } }finally{ lock.unlock(); } }这种方法很简单理解,将需要同步的代码上锁就好了。这个和synchronized差不多都堵塞线程,所以其实都是好消耗资源的操作。需要注意的是使用了锁,需要解锁。解锁最好在finally种执行,以免线程导致死锁。
使用Threadlocal类
private ThreadLocal<Integer> number = new ThreadLocal<Integer>() { //初始值设置 @Override protected Integer initialValue() { return 100000; } }; public void buyTicket() { if(number.get() > 0){ number.set(number.get()-1); } }注意这个已经不是数据共享的问题了。
结果太长不好贴出,描述一下
最后结果:线程名Thread-0 : 剩余火车票90000
最后结果:线程名Thread-1 : 剩余火车票90000
结果就是每个线程都执行了10000遍,但是数据不会相互交互,
从线程的角度看,每个线程都保持一个对其线程局部变量副本的隐式引用,只要线程是活动的并且 ThreadLocal 实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。
通过ThreadLocal存取的数据,总是与当前线程相关,也就是说,JVM 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。
ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单,在ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本。
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
上面那个栗子不能体现这种用法的好处,接下来我再写一个栗子。
ThreadLocalTest.java
public class ThreadLocalTest implements Runnable{ private final static ThreadLocal<User> userTL =new ThreadLocal<>(); public static void main(String[] args) { ThreadLocalTest test = new ThreadLocalTest(); Thread t1 = new Thread(test,"线程a"); Thread t2 = new Thread(test,"线程b"); t1.start(); t2.start(); } @Override public void run() { User user = userTL.get(); if(user == null){ userTL.set(new User()); } System.out.println(userTL.get()); user = userTL.get(); user.setMoney((int)(Math.random()*1000)); for(int i = 0; i < 100; i++){ System.out.println(Thread.currentThread().getName()+":用户拥有金额"+user.getMoney()); } } }
User.java
public class User { private int money; public int getMoney() { return money; } public void setMoney(int money) { this.money = money; } }结果:
线程a:用户拥有金额156 线程b:用户拥有金额710 线程a:用户拥有金额156 线程b:用户拥有金额710 线程a:用户拥有金额156 线程b:用户拥有金额710 线程a:用户拥有金额156 线程b:用户拥有金额710 线程a:用户拥有金额156 线程b:用户拥有金额710
两个用户数据不会因为线程而发生任何交互,这样就达到了线程安全。
相关文章推荐
- Java多线程-线程的同步与锁
- java多线程之线程并发库的其他同步工具类
- Java多线程——Java线程同步问题
- Java多线程-线程的同步(同步代码块)
- Java多线程之线程的同步与锁
- 关于java多线程中同步的问题(两个线程访问同一个实例类的两个同步方法,会不会互相影响)
- java多线程总结(三):线程的同步和通信
- Java多线程-线程的同步与锁
- 从零学习JAVA多线程(三):线程的同步问题
- Java多线程--线程的同步与通信
- Java多线程-线程的同步与锁
- Java多线程-线程的同步(同步方法)
- Java多线程开发五——线程的同步
- Java多线程-线程的同步与锁的问题
- Java多线程-线程的同步与锁
- Java多线程-线程的同步(同步代码块)
- java多线程,java线程同步
- Java多线程-线程的同步与锁
- Java多线程之线程安全与同步实例
- Java多线程-线程的同步与锁