Java JDK1.8(十三) - 多线程之线程安全详解
线程安全
线程安全就是当多个线程访问某一个类(对象或方法)时,这个类始终都能表现出正确的行为,那么这个类(对象或方法)就是线程安全。
public class ThreadSyncProblem { public static void main(String[] args) {
Ticket2 ticket = new Ticket2();
new Thread(ticket,"窗口1").start(); new Thread(ticket,"窗口2").start(); new Thread(ticket,"窗口3").start();
//细心观察结果,会发现会有多个线程同时卖出某张票 //窗口3 - 卖出 10号座 //窗口2 - 卖出 10号座 System.out.println("主线程执行完毕"); } }
class Ticket2 implements Runnable {
/** * 线程共享变量 */ int num = 10;
@Override public void run() { while (num > 0) { System.out.println(Thread.currentThread().getName() + " - 卖出 " + num + "号座"); num--; } System.out.println(Thread.currentThread().getName() + "票已售空"); } } |
/** * 线程安全问题示例2 */ public class J06ThreadSyncProblem2 { public static void main(String[] args) { /* * 多个线程访问run方法时,以排队方式进行处理(按照CPU分配的先后顺序)。 * 一个线程想要执行synchronized修饰的方法: * 1、尝试获得锁 * 2、如果获得锁的线程,就可以执行synchronized代码内容, * 拿不到锁,这个线程就会不断等待、监测、尝试获得这把锁,直到拿到, * 而且是多个线程同时去竞争这把锁(也就是所谓的竞争问题,CPU会飙升) */ TestA testA = new TestA(); new Thread(testA,"t1").start(); new Thread(testA,"t2").start(); new Thread(testA,"t3").start(); } } class TestA implements Runnable {
private int count = 0;
/* * 去掉synchronized会发现,顺序是混乱的。 * @overridden @see java.lang.Runnable#run() */ @Override // public void run() { public synchronized void run() { count++; System.out.println(Thread.currentThread().getName() + ":" + count); } } |
如果有互斥资源(共享资源),多个线程同时抢夺这个资源进行业务逻辑处理时,就会产生线程安全的问题,即总票数是10张,A线程还没有对票总数-1,B线程就进来了,此时B线程拿到的是第10张票,但是这种票又已经给A卖出了。
这种情况在生活中,出现的话,可以让A在卖票时,B不可以卖,只有等A卖完这张票了B才能开始卖下一张票。
在程序中将多条操作共享线程代码封装起来,当有线程在执行这些代码的时候,其他线程不可以参与计算,必须等待把这些代码都执行完毕后,其他线程才可以参与运算。
在Java中提供了Synchronized关键字来解决这个问题。
Java内存图
建阅:https://blog.****.net/Su_Levi_Wei/article/details/80239654
Synchronized(解决线程安全问题)
/** * 线程安全问题 - synchronized解决 */ public class J07ThreadSync { public static void main(String[] args) {
SynchronizedTest ticket = new SynchronizedTest();
new Thread(ticket,"窗口1").start(); new Thread(ticket,"窗口2").start(); new Thread(ticket,"窗口3").start();
//细心观察结果,会发现会有多个线程同时卖出某张票 //窗口3 - 卖出 10号座 //窗口2 - 卖出 10号座 System.out.println("主线程执行完毕"); } }
class SynchronizedTest implements Runnable {
private int num = 100;
private Object object = new Object();
@Override public void run() { // test1(); test2(); }
/** * 1、同步代码块,对指定的代码段使用同步代码块,即这段代码在任何时候都只有一个线程在运行。 * * 同步代码块要运行的话: * 获取到CPU执行权 * 获取到锁 * * 锁可以是任意对象,可以理解为一个标记,true/false * 多个线程必须使用同一把锁 */ public void test1() { synchronized (object) { // synchronized ("字符串") {//▲▲▲ 注:不能是字符串,字符串在内存是位于常量池,只有一份,会出现死循环 while (num > 0) { System.out.println(Thread.currentThread().getName() + " - 卖出 " + num + "号座"); num--; } System.out.println(Thread.currentThread().getName() + "票已售空"); } }
/** * 2、同步方法 * 这个方法在任何时间只能由一个线程在访问,同步方法中也是有锁的,锁就是this对象。 */ public synchronized void test2() { while (num > 0) { System.out.println(Thread.currentThread().getName() + " - 卖出 " + num + "号座"); num--; } System.out.println(Thread.currentThread().getName() + "票已售空"); } } |
/** * 线程安全问题 – synchronized使用方式 */ public class J08ThreadSyncWay { public static void main(String[] args) {
Ticket3 ticket = new Ticket3();
new Thread(ticket,"窗口1").start(); new Thread(ticket,"窗口2").start(); new Thread(ticket,"窗口3").start();
//细心观察结果,会发现会有多个线程同时卖出某张票 //窗口3 - 卖出 10号座 //窗口2 - 卖出 10号座 System.out.println("主线程执行完毕"); } }
class Ticket3 implements Runnable {
private Object object = new Object();
private int num = 100;
@Override public void run() { // test1(); // test2(); // test3(); // test4(); test5(); }
/** * 1、对象锁 - 代码级别 * 统一对象争用该锁,this为Ticket3实例,synchronized的锁绑定在this对象上 */ public void test1() { synchronized (this) { while (num > 0) { System.out.println(Thread.currentThread().getName() + " - 卖出 " + num + "号座"); num--; } System.out.println(Thread.currentThread().getName() + "票已售空"); } }
/** * 2、对象锁 - 方法级别 * 同一类争用该锁,普通(非静态方法),synchronized的锁绑定在调用该方法的对象上。 */ public synchronized void test2() { while (num > 0) { System.out.println(Thread.currentThread().getName() + " - 卖出 " + num + "号座"); num--; } System.out.println(Thread.currentThread().getName() + "票已售空"); }
/** * 3、对象锁 - 代码级别 * 同一类争用该锁,绑定在object上,不同this(Ticket3),拥有同一个object对象 */ public void test3() { synchronized (object) { while(num > 0 ) { System.out.println(Thread.currentThread().getName() + " - 卖出 " + num + "号座"); num--; } System.out.println(Thread.currentThread().getName() + "票已售空"); } }
/** * 4、类锁 - 代码级别 * 同一类对象争用该锁 */ public void test4(){ synchronized (Ticket3.class) { while(num > 0 ) { System.out.println(Thread.currentThread().getName() + " - 卖出 " + num + "号座"); num--; } System.out.println(Thread.currentThread().getName() + "票已售空"); } }
/** * 5、类锁 - 方法级别 * 同一类争用该锁,synchronized的锁绑定在Ticket3.class上,即静态方法的锁就是类对象 */ public static synchronized void test5() { for (int i = 0; i < 200; i++) { System.out.println(Thread.currentThread().getName() + " - " + i); } } } |
同步的前提:
两个或两个以上的线程,单线程无需同步。
多个线程使用的是同一把锁,即使获得CPU执行权,没有获得锁也无法执行。
同步的优缺点:
优点:解决了多线程使用同一资源(互斥)发生的问题。
缺点:相对的降低效率,同步的线程都会判断锁,不断的去获取锁,额外的
还有维持这个锁的资源。
同步vs异步
同步(synchronized):就是共享资源,如果不是共享资源,就没有必要进行同步了。
异步(asynchronized):异步就是独立的,相互之间不受任何制约因素的影响,就类似在用ajax发请求后,还可以继续浏览或操作页面内容,二者之间没有任何关系。
/** * 同步与异步: * 同步:A持有对象的锁,B线程如果调用对象中的synchronized的方法则需要等待。 * 异步:A持有对象的锁,B线程调用对象中非synchronized修饰的方法不用等待。 */ public class J09SyncWithASync {
public static void main(String[] args) { SyncTest syncTest = new SyncTest();
new Thread(new Runnable() { @Override public void run() { try { syncTest.m1(); } catch (InterruptedException e) { e.printStackTrace(); } } },"t1").start();
new Thread(new Runnable() { @Override public void run() { syncTest.m2(); } },"t2").start(); } } class SyncTest { public synchronized void m1() throws InterruptedException { System.out.println(Thread.currentThread().getName() + " - m1"); Thread.sleep(3000); System.out.println(Thread.currentThread().getName() + " - m1T"); }
public void m2() { System.out.println(Thread.currentThread().getName() + " - m2"); } } |
Java多线程内存互斥资源图
/** * 多个线程多个锁: * 每个线程都可以拿到自己指定的锁,分别获得锁之后执行synchronized方法体的内容。 * 可以理解就是一个Thread就是一个线程,而这个线程执行的对象是不是同一个对象就决定了是否会有共享资源的问题。 * 对象之间是具有隔离性的。 */ public class J08ThreadSyncZManyLock { public static void main(String[] args) { ManyLock m1 = new ManyLock(); ManyLock m2 = new ManyLock();
new Thread(new Runnable() { @Override public void run() { m1.printNum("m1"); } }).start();
new Thread(new Runnable() { @Override public void run() { m2.printNum("m2"); } }).start();
/* * 关键字synchronized取得的锁都是对象锁,而不是把一段代码(方法)当做锁, * 所以示例代码中,那个线程先执行synchronized关键字的方法, * 那个线程就持有该方法所属对象的锁,两个对象,线程获得就是不同的锁,他们互不影响。 */
} } class ManyLock {
private int num = 0;
public synchronized void printNum(String tag) { try { if(tag.equals("m1")) { num = 100; Thread.sleep(5000); }else { num = 200; } System.out.println(tag + " - " + num); } catch (Exception e) { e.printStackTrace(); } } } |
Synchronized细节
/** *脏读 * 对于对象的同步和异步的方法,在设计程序时, * 一定要考虑问题的整体性,否则会出现数据不一致的问题,很经典的就是脏读 * 即:如果A方法和B方法,A方法加了synchronized而B方法没有加, * A方法还没执行完之前,B方法去获取数据,获取到的数据是不正确的,而在oracle数据库是用undo(暂存区)的方式解决。 */ public class J10SyncDeail01DirtyRead { public static void main(String[] args) throws InterruptedException { DirtyRead dirtyRead = new DirtyRead();
new Thread(new Runnable() { @Override public void run() { dirtyRead.setValue("Ami", 22); } }).start();
//sleep:会去持有锁的。 Thread.sleep(2000);
dirtyRead.getValue(); } } class DirtyRead {
private String name; private int age;
public synchronized void setValue(String name,int age) { this.name = name; try { Thread.sleep(3000); } catch (Exception e) { } this.age = age; System.out.println("set:" + name + " - " + age); }
/* * 加上synchronized即可解决这个问题 * 同时枷锁synchronized同步关键字,保证业务的原子性。 * 数据库特性:ACID,原子性、一致性、隔离性、持久性。 * 隔离级别:脏读、幻读、不可重复读、序列。 */ public void getValue() { // public synchronized void getValue() { System.out.println("get:" + name + " - " + age); } } |
/** * 锁重入 */ public class J10SyncDeail02LockReentry { public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { new LockReentry().m1(); } }).start(); } } class LockReentry { public synchronized void m1() { System.out.println("m1"); m2(); }
public synchronized void m2() { System.out.println("m2"); m3(); }
public void m3() { System.out.println("m3"); } } |
/** * 子类调用父类一样也是线程安全的 */ public class J10SyncDeail03ParentClass { public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { new Son().operationS(); } }).start(); } } class Parent{ public int i = 10;
public synchronized void operationP(){ try { i--; System.out.println("Parent:" + i); Thread.sleep(1000); } catch (Exception e) { } } } class Son extends Parent { public synchronized void operationS() { try { while (i > 0) { i--; System.out.println("Son:" + i); Thread.sleep(1000); this.operationP(); } } catch (Exception e) { } } } |
/** * 异常释放锁的情况下,如果不及时处理,很有可能对应用程序的业务逻辑产产生严重的错误, * 比如你现在只想一个队列任务,很多对象都去在等待第一个对象正确只想完毕再去释放锁, * 但是第一个对象由于异常的情况出现,导致业务逻辑没有正常的执行完毕就释放锁, * 那么可想而知后续的业务执行都是错误的逻辑。 */ public class J10SyncDeail04Exception { public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { new ExceptionTest().operation(); } }).start(); } } class ExceptionTest { public synchronized void operation(){ try { for (int i = 0; i < 10; i++) { System.out.println(i); if(i == 5) { throw new RuntimeException("抛出异常"); } } } catch (Exception e) { e.printStackTrace(); } } }
|
/** * synchronized代码块加锁,比较灵活 */ public class J10SyncDeail05Block { public static void main(String[] args) { Block block = new Block();
new Thread(new Runnable() { @Override public void run() { try { block.m1(); } catch (InterruptedException e) { e.printStackTrace(); } } }).start();
new Thread(new Runnable() { @Override public void run() { try { block.m2(); } catch (InterruptedException e) { e.printStackTrace(); } } }).start();
new Thread(new Runnable() { @Override public void run() { try { block.m3(); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } } class Block {
public synchronized void m1() throws InterruptedException{ synchronized (this) { //对象锁 System.out.println("m1"); Thread.sleep(2000); } }
public synchronized void m2() throws InterruptedException{ synchronized (Block.class) { //类锁 System.out.println("m2"); Thread.sleep(2000); } }
/** * synchronized可以使用任意的Object进行加锁用法比较灵活。 */ private Object lockObject = new Object();
public synchronized void m3() throws InterruptedException{ synchronized (lockObject) { //任何对象锁 System.out.println("m3"); Thread.sleep(2000); } } } |
/** * 锁对象改变的问题 * 但使用一个对象进行加锁的时候,要注意对象本身发生变化的时候,那么持有的锁就不同了, * 如果对象本身不发生改变,那么依然是同步的,即使对象的属性发生改变。 */ public class J10SyncDeail07ChangeLock { public static void main(String[] args) throws InterruptedException {
//============= 锁变化 Start ChangeLock changeLock = new ChangeLock();
Thread t1 = new Thread(new Runnable() { @Override public void run() { try { changeLock.m1(); } catch (InterruptedException e) { e.printStackTrace(); } } },"t1");
Thread.sleep(100);
Thread t2 = new Thread(new Runnable() { @Override public void run() { try { changeLock.m1(); } catch (InterruptedException e) { e.printStackTrace(); } } },"t2");
//结果:仔细观察,第二轮会发现t1、t2几乎同时进入m1()方法,即锁本身发生变化,就会影响同步。 // t1.start(); // t2.start();
//============= 锁变化 End
//============= 属性变化 Start ChangeLock changeLock2 = new ChangeLock(); new Thread(new Runnable() { @Override public void run() { try { changeLock2.changeName("A"); } catch (InterruptedException e) { e.printStackTrace(); } } },"t1").start();
Thread.sleep(5000);
new Thread(new Runnable() { @Override public void run() { try { changeLock2.changeName("B"); } catch (InterruptedException e) { e.printStackTrace(); } } },"t2").start();
//结果:会发现这个属性改变是不影响锁的。 //============= 属性变化 End } } class ChangeLock {
private String lock = "lock";
/** * 锁变化 */ public synchronized void m1() throws InterruptedException{ synchronized (lock) { System.out.println(Thread.currentThread().getName()); lock = "change"; Thread.sleep(2000); System.out.println(Thread.currentThread().getName()); } }
private String name;
public void setName(String name) { this.name = name; }
public String getName() { return name; }
/** * 改变属性 */ public synchronized void changeName(String name) throws InterruptedException { this.setName(name);
System.out.println(Thread.currentThread().getName() + " = " + this.getName()); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + " End"); } } |
/** * 减少锁的粒度: * 使用synchronized声明的方法在某些情况下是有弊端的。 * 比如A线程调用同步方法执行一个很长时间的任务,那么B线程就必须等待比较长时间才能执行, * 这样的情况下可以使用synchronized代码块去优化代码块执行时间,也就是通常说的减少锁的粒度。 */ public class J10SyncDeail08LockSize { public static void main(String[] args) throws InterruptedException { LockSize lockSize = new LockSize(); new Thread(new Runnable() { @Override public void run() { try { lockSize.m1(); } catch (InterruptedException e) { e.printStackTrace(); } } },"t1").start();
new Thread(new Runnable() { @Override public void run() { try { lockSize.m1(); } catch (InterruptedException e) { e.printStackTrace(); } } },"t2").start();
} } class LockSize {
/** * 锁变化 */ public void m1() throws InterruptedException{ System.out.println("Start - " + Thread.currentThread().getName());
if("t1".equals(Thread.currentThread().getName())) { Thread.sleep(3000); }else { Thread.sleep(5000); }
synchronized (this) { System.out.println(Thread.currentThread().getName() + " - 执行代码块"); Thread.sleep(2000); } System.out.println("End - " + Thread.currentThread().getName()); } } |
补充:内部类和线程内使用的局部变量一般为final,如果不为final,内部构造类构造完成后,外部类的局部变量改变了怎么办,而在1.8中默认了final,所以不用写。
JDK1.5-Lock
以前使用synchronized关键字加锁、解锁、监视器都是不可见的。
JDK1.5开始,把锁和监视器都做成了对象,让这2个对象的操作都是可见的。
/** * 案例一:生产者-消费者 * 采用JDK1.5的Lock的方式 */ public class J16MessagePCByLock { public static void main(String[] args) { ProductA productA = new ProductA(); ProducerA producer = new ProducerA(productA); ConsumerA consumer = new ConsumerA(productA); new Thread(producer,"p1").start(); new Thread(producer,"p2").start(); new Thread(consumer,"c1").start(); new Thread(consumer,"c2").start(); } } /** * 商品 */ class ProductA {
/** * 是否有商品 */ boolean isHashProduct = false;
/** * 自定义锁 */ Lock lock = new ReentrantLock();
/** * 监视器 */ Condition condition = lock.newCondition();
/** * 生产 */ public void product() throws InterruptedException { //生产,上锁 lock.lock();
while (isHashProduct) { //已有商品,进入等待,放弃CPU执行权、放弃锁 condition.await(); }
isHashProduct = true; Thread.sleep(3000); System.out.println("生产一台笔记本");
//唤醒消费者 condition.signalAll(); //生产完成,释放锁 lock.unlock(); } /** * 消费者 */ public void consumer() throws InterruptedException { //消费上锁 lock.lock();
while (!isHashProduct) { //没有商品,进入等待 condition.await(); }
isHashProduct = false; Thread.sleep(3000); System.out.println("消费一台笔记本");
//唤醒生产者 condition.signalAll(); lock.unlock(); } } class ProducerA implements Runnable {
/** * 生产者 */ ProductA productA;
public ProducerA(ProductA productA) { super(); this.productA = productA; }
@Override public void run() { try { productA.product(); } catch (InterruptedException e) { e.printStackTrace(); } } } class ConsumerA implements Runnable {
/** * 生产者 */ ProductA productA;
public ConsumerA(ProductA productA) { super(); this.productA = productA; }
@Override public void run() { try { productA.consumer(); } catch (InterruptedException e) { e.printStackTrace(); } } } |
/** * 案例二:生产者-消费者 * 采用JDK1.5的Lock的方式:多个监视器 */ public class J17MessagePCByLockMC { public static void main(String[] args) { ProductB productB = new ProductB(); ProducerB producer = new ProducerB(productB); ConsumerB consumer = new ConsumerB(productB); new Thread(producer,"p1").start(); new Thread(producer,"p2").start(); new Thread(consumer,"c1").start(); new Thread(consumer,"c2").start(); } } /** * 商品 */ class ProductB {
/** * 是否有商品 */ boolean isHashProduct = false;
/** * 自定义锁 */ Lock lock = new ReentrantLock();
/** * 监视器 */ Condition pCondition = lock.newCondition(); Condition cCondition = lock.newCondition();
/** * 生产 */ public void product() throws InterruptedException { //生产,上锁 lock.lock();
while (isHashProduct) { //已有商品,进入等待,放弃CPU执行权、放弃锁 pCondition.await(); }
isHashProduct = true; Thread.sleep(3000); System.out.println("生产一台笔记本");
//唤醒消费者 cCondition.signal(); //生产完成,释放锁 lock.unlock(); } /** * 消费者 */ public void consumer() throws InterruptedException { //消费上锁 lock.lock();
while (!isHashProduct) { //没有商品,进入等待 cCondition.await(); }
isHashProduct = false; Thread.sleep(3000); System.out.println("消费一台笔记本");
//唤醒生产者 pCondition.signal(); lock.unlock(); } } class ProducerB implements Runnable {
/** * 生产者 */ ProductB productB;
public ProducerB(ProductB productB) { super(); this.productB = productB; }
@Override public void run() { try { productB.product(); } catch (InterruptedException e) { e.printStackTrace(); } } } class ConsumerB implements Runnable {
/** * 生产者 */ ProductB productB;
public ConsumerB(ProductB productB) { super(); this.productB = productB; }
@Override public void run() { try { productB.consumer(); } catch (InterruptedException e) { e.printStackTrace(); } } } |
Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁操作,允许定义更灵活的结构,可以具有差别很大的属性,和支出多个相关的Condition对象。
synchronized在获取锁和解锁的过程是不可见的,Lock的获取锁和解锁过程是可见的。
synchronized的监视器也是不可见的,Lock的监视器是可见的。
案例二中可以看到一把锁可以对应多个监视器。
以上的两个案例中可以感受到,Lock局限性降低了,更加灵活,功能也更加强大。