Java线程同步及实现方法详解
作者:y_initiate
1. 什么是线程同步?
首先,引用一个非常经典的例子来说明为什么要进行线程同步
当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常。 举个例子,动物园有三个窗口同时在售卖门票,假设还剩最后一张门票时,有两个窗口同时有人在买门票,此时两个窗口都观察到还有一张门票,于是两个窗口都选择了卖出,此时门票数变成了-1,出现错误。
还可能会出现其他情况的错误,比如剩余10张票时,两个窗口同时售卖出一张票后修改票数为9。
package test; import java.io.*; public class TicketThreadTest { public static void main(String[] args) throws IOException, InterruptedException { TicketThread ticket1 = new TicketThread(); Thread thread1 = new Thread(ticket1, "窗口1"); Thread thread2 = new Thread(ticket1, "窗口2"); Thread thread3 = new Thread(ticket1, "窗口3"); thread1.start(); thread2.start(); thread3.start(); thread1.join(); // 等待三个线程结束后打印卖出总数 thread2.join(); thread3.join(); System.out.println("sellNum: " + TicketThread.sellNum); } } class TicketThread implements Runnable { private static int ticketNum = 20; // 总票数 public static int sellNum = 0; // 统计卖出总票数 @Override public void run() { while (true) { if (ticketNum > 0) { System.out.println(Thread.currentThread().getName() + "卖出了一张票,剩余:" + --ticketNum); // 卖出一张票 sellNum++; // 卖出总票数加1 } else { break; } try { Thread.sleep(10); // 每次sleep 10ms,提高出错可能 } catch (InterruptedException e) { throw new RuntimeException(e); } } } }
这是某次程序运行结果,很显然卖出29张票,发生了错误
窗口3卖出了一张票,剩余:19
窗口1卖出了一张票,剩余:18
窗口2卖出了一张票,剩余:17
窗口3卖出了一张票,剩余:16
窗口1卖出了一张票,剩余:16
窗口2卖出了一张票,剩余:16
窗口1卖出了一张票,剩余:15
窗口3卖出了一张票,剩余:14
窗口2卖出了一张票,剩余:15
窗口2卖出了一张票,剩余:13
窗口3卖出了一张票,剩余:12
窗口1卖出了一张票,剩余:13
窗口3卖出了一张票,剩余:9
窗口1卖出了一张票,剩余:11
窗口2卖出了一张票,剩余:10
窗口1卖出了一张票,剩余:7
窗口3卖出了一张票,剩余:8
窗口2卖出了一张票,剩余:8
窗口1卖出了一张票,剩余:5
窗口3卖出了一张票,剩余:6
窗口2卖出了一张票,剩余:6
窗口2卖出了一张票,剩余:4
窗口3卖出了一张票,剩余:3
窗口1卖出了一张票,剩余:4
窗口1卖出了一张票,剩余:2
窗口3卖出了一张票,剩余:2
窗口2卖出了一张票,剩余:1
窗口2卖出了一张票,剩余:-1
窗口1卖出了一张票,剩余:0
sellNum: 29
2. Java线程同步方法
Java线程同步有7种方法
- 使用 synchronized关键字实现线程同步
- 使用wait和notify实现线程同步
- 使用特殊域变量(volatile)实现线程同步
- 使用重入锁实现线程同步,在JavaSE5.0中新增了一个java.util.concurrent包来支持同步
- 使用局部变量实现线程同步,如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
- 使用阻塞队列实现线程同步
- 使用原子变量实现线程同步
3 使用synchronized实现线程同步
synchronized的作用主要有三个:
- 原子性:确保线程互斥地访问同步代码
- 可见性:保证共享变量的修改能够及时可见
可见性是通过Java内存模型中的“对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值”来保证的
- 有序性:有效解决重排序问题,即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”
happen-before:If one action _happens-before _another, then the first is visible to and ordered before the second. 如果指令甲happens-before指令乙,那么指令甲必须排序在指令乙之前,并且指令甲的执行结果对指令乙可见。
3.1 同步代码块
同步代码块是通过锁定一个指定的对象,来对同步代码块中的代码进行同步。 一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该代码块的线程将被阻塞。 注意synchronized必须锁住的是指定的对象,不同对象间不会阻塞,如果需要锁住类对象,只需要使用synchronized(Class clazz)锁住类即可。
我们使用同步代码块来解决售票问题
package test; import java.io.*; public class TicketThreadTest { public static void main(String[] args) throws IOException, InterruptedException { TicketThread ticket1 = new TicketThread(); Thread thread1 = new Thread(ticket1, "窗口1"); Thread thread2 = new Thread(ticket1, "窗口2"); Thread thread3 = new Thread(ticket1, "窗口3"); thread1.start(); thread2.start(); thread3.start(); thread1.join(); // 等待三个线程结束后打印卖出总数 thread2.join(); thread3.join(); System.out.println("sellNum: " + TicketThread.sellNum); } } class TicketThread implements Runnable { private static int ticketNum = 20; // 总票数 public static int sellNum = 0; // 统计卖出总票数 @Override public void run() { while (true) { synchronized (this) { // 锁住对共享变量的访问 if (ticketNum > 0) { System.out.println(Thread.currentThread().getName() + "卖出了一张票,剩余:" + --ticketNum); // 卖出一张票 sellNum++; // 卖出总票数加1 } else { break; } } try { Thread.sleep(10); // 每次sleep 10ms,提高出错可能 } catch (InterruptedException e) { throw new RuntimeException(e); } } } }
从运行结果可以看到synchronized修饰的代码块同一时间只能有一个线程访问
窗口1卖出了一张票,剩余:19
窗口2卖出了一张票,剩余:18
窗口3卖出了一张票,剩余:17
窗口3卖出了一张票,剩余:16
窗口2卖出了一张票,剩余:15
窗口1卖出了一张票,剩余:14
窗口1卖出了一张票,剩余:13
窗口2卖出了一张票,剩余:12
窗口3卖出了一张票,剩余:11
窗口2卖出了一张票,剩余:10
窗口3卖出了一张票,剩余:9
窗口1卖出了一张票,剩余:8
窗口1卖出了一张票,剩余:7
窗口2卖出了一张票,剩余:6
窗口3卖出了一张票,剩余:5
窗口2卖出了一张票,剩余:4
窗口3卖出了一张票,剩余:3
窗口1卖出了一张票,剩余:2
窗口3卖出了一张票,剩余:1
窗口1卖出了一张票,剩余:0
sellNum: 20
注意上述类中的ticketNum和sellNum都属于类对象,如果我们使用不同的实例对象,使用synchronized(this)锁住的不是同一个对象,会发现并没有实现线程同步,此时就需要锁住synchronized(this.getClass())。
使用不同实例对象,main方法中修改如下:
//使用不同实例对象,main方法中修改如下: TicketThread ticket1 = new TicketThread(); TicketThread ticket2 = new TicketThread(); TicketThread ticket3 = new TicketThread(); Thread thread1 = new Thread(ticket1, “窗口1”); Thread thread2 = new Thread(ticket2, “窗口2”); Thread thread3 = new Thread(ticket3, “窗口3”); //TicketThread类中修改为 synchronized(this.getClass())或synchronized(TicketThread.class)
3.2 同步方法
同步方法是对这个方法块里的代码进行同步,而这种情况下锁定的对象就是方法所属的对象自身。
相当于使用synchronized(this)锁住方法中的代码
如果这个方法是静态同步方法呢?那么线程锁定的就不是这个类的对象了,而是这个类对应的java.lang.Class类型的对象。
相当于使用synchronized(this.getClass())锁住方法中的代码
**注意:**当一个同步方法或者同步块被某个线程执行时,这个对象就被锁定了,其他线程无法在此时访问这个对象的同步方法,也不能执行同步块,但可以访问非同步方法中的非同步代码块。
上述售票问题使用同步方法实现线程同步
package test; import java.io.*; public class TicketThreadTest { public static void main(String[] args) throws IOException, InterruptedException { TicketThread ticket1 = new TicketThread(); Thread thread1 = new Thread(ticket1, "窗口1"); Thread thread2 = new Thread(ticket1, "窗口2"); Thread thread3 = new Thread(ticket1, "窗口3"); thread1.start(); thread2.start(); thread3.start(); thread1.join(); // 等待三个线程结束后打印卖出总数 thread2.join(); thread3.join(); System.out.println("sellNum: " + TicketThread.sellNum); } } class TicketThread implements Runnable { private static int ticketNum = 20; // 总票数 public static int sellNum = 0; // 统计卖出总票数 @Override public void run() { while (true) { if(!sellOneTicket()){ break; } try { Thread.sleep(10); // 每次sleep 10ms,提高出错可能 } catch (InterruptedException e) { throw new RuntimeException(e); } } } private synchronized boolean sellOneTicket(){ if (ticketNum > 0) { System.out.println(Thread.currentThread().getName() + "卖出了一张票,剩余:" + --ticketNum); // 卖出一张票 sellNum++; // 卖出总票数加1 return true; } else { return false; } } }
到此这篇关于Java线程同步及实现方法详解的文章就介绍到这了,更多相关Java线程同步内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!