java多线程之线程同步

来源:互联网 发布:做java培训讲师怎么样 编辑:程序博客网 时间:2024/04/30 20:04

线程不同步问题的出现

当处理共享资源的时候,修改数据和读取数据的同时,多线程不同步会出现脏数据,注意脏数据只是数据错处,并不是说代码逻辑有问题,代码本身是没有什么问题的。举个栗子,共享数据为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;@Overridepublic 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();}@Overridepublic 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

两个用户数据不会因为线程而发生任何交互,这样就达到了线程安全。










1 0