线程同步:
多线程编程三大问题:
分工:任务拆分,哪些线程干哪些任务
同步:多个线程一起做一件事情,线程间通信(线程合作)
互斥:多线程并发时,只能有一个线程访问共享资源
问题引入(一张票多次卖出问题):
还剩一张票 //线程1
还剩-1张票 //线程2
还剩0张票 //线程3
public void run() { //ticket =1 ; while (this.ticket > 0) { //线程1 2 3 try { //线程2 //线程3 //线程1 Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } //线程1 ticket = 0; //线程1 ticket = -1; //线程1 ticket = -2; System.out.println("还剩下"+ticket--+"票"); } }
锁:
一把锁保护一个相应的资源,不同锁保护不同的对象
java中锁的实现:
使用sychroinzed关键字为程序逻辑上锁
两种用法:
(1)同步代码块
sychroinzed(锁的对象(this,也就是当前对象)|Object及其子类|类对象(当前类.class)) {
//此处代码块在任意一个时刻只能有一个线程进入
}
class TicketTask implements Runnable { private int ticket = 20; @Override public void run() { for (int i = 0; i < 20; i++) { //需要在判断处上锁 synchronized (this){ //在任意时刻只有一个线程能进入条件判断 if (ticket >0) { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+ "还剩下"+ticket--+"票"); } } } } } public class SellTicket { public static void main(String[]args){ TicketTask ticketTask = new TicketTask(); Thread thread1 = new Thread(ticketTask,"黄牛A"); Thread thread2 = new Thread(ticketTask,"黄牛B"); Thread thread3 = new Thread(ticketTask,"黄牛C"); thread1.start(); thread2.start(); thread3.start(); } }
(2)同步方法
(1)修饰普通对象方法 public synchronized void sellTicket() ; 默认锁的是当前对象this
直接在方法声明上使用sychroinzed,此时表示同步方法在任一时刻只能有一个线程进入
class TicketTask implements Runnable { private int ticket = 20; @Override public void run() { for (int i = 0; i < 20; i++) { sellTicket(); } } //在任一时刻,只能有一个线程进入此方法 public synchronized void sellTicket() { if (ticket > 0) { System.out.println(Thread.currentThread().getName()+ "还剩下"+ticket--+"票"); } } } public class SellTicket { public static void main(String[]args){ TicketTask ticketTask = new TicketTask(); Thread thread1 = new Thread(ticketTask,"黄牛A"); Thread thread2 = new Thread(ticketTask,"黄牛B"); Thread thread3 = new Thread(ticketTask,"黄牛C"); thread1.start(); thread2.start(); thread3.start(); } }
sychroinzed对象锁
class Sync { public synchronized void test() throws InterruptedException { System.out.println(Thread.currentThread().getName()+"正在打印..."); Thread.sleep(1000); System.out.println(Thread.currentThread().getName()+"执行结束..."); } } class Task implements Runnable{ Sync sync = new Sync(); //改进方法一 @Override public void run() { //此处的synchronized并不能锁住,因为会产生三个Sync对象,也就是三把锁 // Sync sync = new Sync(); try { sync.test(); } catch (InterruptedException e) { e.printStackTrace(); } } } public class SyncTest { public static void main(String[]args){ Task task = new Task(); new Thread(task).start(); new Thread(task).start(); new Thread(task).start(); } }
(2)修饰的是静态方法,锁当前类.class
若 synchronized 修饰的是静态方法或者synchronized (类名称.class) synchronized 锁的都是当前类的反射对象(全局唯一)
class Sync { //改进方法二 public static synchronized void test() throws InterruptedException { System.out.println(Thread.currentThread().getName()+"正在打印..."); Thread.sleep(1000); System.out.println(Thread.currentThread().getName()+"执行结束..."); } } class Task implements Runnable{ @Override public void run() { Sync sync = new Sync(); try { sync.test(); } catch (InterruptedException e) { e.printStackTrace(); } } } public class SyncTest { public static void main(String[]args){ Task task = new Task(); new Thread(task).start(); new Thread(task).start(); new Thread(task).start(); } }
class Sync { //同一时刻,线程一进入test方法,别的线程并不能进入test2,此时同一把锁保护了两个方法 public synchronized void test() throws InterruptedException { System.out.println(Thread.currentThread().getName()+"正在打印..."); Thread.sleep(1000); System.out.println(Thread.currentThread().getName()+"执行结束..."); } public synchronized void test2() {} } class Task implements Runnable{ Sync sync = new Sync(); @Override public void run() { try { sync.test(); } catch (InterruptedException e) { e.printStackTrace(); } } } public class SyncTest { public static void main(String[]args){ Task task = new Task(); new Thread(task).start(); new Thread(task).start(); new Thread(task).start(); } }
synchronized 底层实现:
在使用 synchronized 时必须保证锁定对象必须为Object及其子类对象
synchronized 使用的是JVM层级别的Monitor与MonitorExit实现
这两个指令必须获取对象的同步监视器Monitor
对象Monitor机制:
第一步:monitorenter:检查O对象的Monitor计数器值是否为0,为0表示此监视器还未被任意一个线程获取,此线程可以进入同步代码块并且将Monitor值+1,将monitor的持有线程标记为当前线程。
情况1:当Monitor计数器值不为0且持有线程不是当前线程,表示Monitor已被别的线程占用,当前线程只能阻塞等待。
情况2:当Monitor计数器值不为0但是持有的线程恰好是当前线程,
线程2
第二步(进入同步代码块): synchronized(O) {
//线程1
}
第三步(退出):monitorexit (线程释放 monitor计数器-1)
可重入锁:
当执行MonitorEnter时,对象的Monitor计数器值不为0,但是持有的线程恰好是当前线程,此时将Monitor计数器的值再次+1,当前线程继续进入同步方法或代码块。
synchronized void TestA(){ //线程A //TestB() 这里就相当于可重入锁,可以进入 } synchronized void TestB(){ //线程B能否同时进入同一个对象的TestB(); (不能) }
JDK1.6 之后synchronized 优化(了解)
synchronized 互斥,
之前synchronized 获取锁失败,将线程挂起—悲观锁策略
优化让每个线程通过代码块的速度提高
1、CAS操作(无锁实现的同步-乐观锁)—自旋:
class Test { int i = 0; synchronized (this) { i = 10; } }
compareAndSwap(O,V,N)
O:当前线程存储的变量值 0
V:内存中该变量的具体指 10
N:希望修改后的变量值 10
当O==V时,此时表示还没有线程修改共享变量的值,此时可以成功的将内存中的值修改为N
当O!= V时,表示此时内存中的共享变量值已被其他线程修改,此时返回内存中最新值V,再次尝试修改变量
线程挂起阻塞:车熄火
自旋:脚踩刹车,不熄火
导致的问题:
(1)ABA问题:解决方法:添加版本号
(2)自旋在CPU上跑无用指令,会浪费CPU资源,
自旋适应:JVM尝试自旋一段时间,若在时间内成功获取到锁,在下次获取锁时,适当延长自旋时间
若在时间内,线程没有获取到锁,在下次获取时,适当缩短自旋时间
(3)公平性问题:处于阻塞状态线程可能会一直无法获取到锁
Lock锁可以实现公平性,synchronized 无法实现公平锁
2、偏向锁:JDK1.6之后默认synchronized
最乐观的锁,进入同步代码块或者方法的始终是一个线程
当出现另一个线程也尝试获取锁(在不同时刻)时,偏向锁会升级为轻量级锁
3、轻量级锁:不同的时刻有不同的线程尝试获取锁,“亮黄灯策略”
同一时刻不同线程尝试获取锁,会将偏向锁升级为重量级锁
4、重量级锁 :JDK1.6之前的synchronized 都是重量级锁,将线程阻塞挂起(JDK1.6自适应自旋)
锁只有升级过程没有降级过程
5、锁粗化:当出现多次的加锁与解锁过程,会将多次的加锁解锁过程粗化为一次的加锁与解锁过程。
public class Test{ private static StringBuffer sb = new StringBuffer(); public static void main(String[] args) { sb.append("a"); sb.append("b"); sb.append("c"); } }
6、锁消除
当对象不属于共享资源时,对象内部的同步方法或同步代码块会被自动解除
锁消除即即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那
么可以认为这段代码是线程安全的,不必要加锁。
public class Test{ public static void main(String[] args) { StringBuffer sb = new StringBuffer(); sb.append("a").append("b").append("c"); } }
虽然StringBuffer的append是一个同步方法,但是这段程序中的StringBuffer属于一个局部变量,并且不会从该方法中
逃逸出去,所以其实这过程是线程安全的,可以将锁消除。