线程安全、锁

多线程理解(一)
多线程理解(二)
并发工具类
JVM内存结构、Java内存模型、Java对象模型
本文参考了黑马程序员的课程,全面深入学习Java并发编程,JUC并发编程全套教程

线程安全

当多个线程访问一个对象时,如果不用考虑这些线程在运行环境下的调度和交替执行,也不需要进行额外的同步,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的!

非线程安全问题存在于实例变量中,对于方法内部的私有变量,则不存在非线程安全问题。

线程安全示例:a++多线程下出现消失的情况

public class MultiThreadError implements Runnable{
    static int a = 0;
    
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            a++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MultiThreadError threadError = new MultiThreadError();
        Thread thread1 = new Thread(threadError);
        Thread thread2 = new Thread(threadError);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(a);
    }
}

多次运行程序,发现a最终的值不总是正确的!

这是为什么呢?,如下图所示:

在这里插入图片描述
线程1与线程2之间的操作是互不知晓的:线程1执行完 i+1的操作后,切换到线程2,线程2也执行完 i+1的操作后再次切回到线程1,线程1将 i 写为2,同理再次切换到线程2后,线程2也将 i 写为2。

所以就出现了两次加1的操作,但只生效了一次,导致最终运行结果会比实际结果药效;

死锁

当两个线程相互等待对方释放同步监视器时就会发生死锁;
在这里插入图片描述
死锁示例

public class MultiThreadError implements Runnable{
    int flag = 1;
    static Object object1 = new Object();
    static Object object2 = new Object();

    @Override
    public void run() {
        if (flag == 1){
            synchronized (object1){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (object2){
                    System.out.println(flag);
                }
            }
        }
        if (flag == 2){
            synchronized (object2){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (object1){
                    System.out.println(flag);
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MultiThreadError threadError1 = new MultiThreadError();
        MultiThreadError threadError2 = new MultiThreadError();
        threadError1.flag = 1;
        threadError2.flag = 2;
        new Thread(threadError1).start();
        new Thread(threadError2).start();
    }
}

线程1锁住 object1 然后等待获取object2,同时,线程2锁住 object2 然后等待获取object1。它们互相等待对方先释放锁最终就会造成死锁!

死锁发生的4个必要条件

  1. 互斥条件:一个资源每次只能被一个进程使用
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
  3. 不可剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁!

如何定位死锁?

1、jstack命令

先通过jps命令获取到pid,再通过jstack [pid] 获取到具体信息

Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x000000001cc435b8 (object 0x000000076b4d85d0, a java.lang.Object),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x000000001cc40d28 (object 0x000000076b4d85e0, a java.lang.Object),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
        at com.example.demo.MultiThreadError.run(MultiThreadError.java:34)
        - waiting to lock <0x000000076b4d85d0> (a java.lang.Object)
        - locked <0x000000076b4d85e0> (a java.lang.Object)
        at java.lang.Thread.run(Thread.java:748)
"Thread-0":
        at com.example.demo.MultiThreadError.run(MultiThreadError.java:22)
        - waiting to lock <0x000000076b4d85e0> (a java.lang.Object)
        - locked <0x000000076b4d85d0> (a java.lang.Object)
        at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

通过以上信息,我们可以发现死锁产生的具体原因与位置;

2、ThreadMXBean类

在死锁示例最后加上如下代码:

ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
if (deadlockedThreads != null && deadlockedThreads.length > 0 ){
    for (int i = 0; i < deadlockedThreads.length; i++) {
        ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[i]);
        System.out.println("发现死锁"+threadInfo.getThreadName());
    }
}

运行程序,控制台会打印出发生死锁的相关信息;

锁状态

锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态;

Java对象头

由于Java面向对象的思想,在JVM中需要大量存储对象,存储时为了实现一些额外的功能,需要在对象中添加一些标记字段用于增强对象功能,这些标记字段组成了对象头;

j对象头包含内容如下:

内容描述
Mark Word存储对象的hashCode或锁信息
Class Metadata Address存储到对象的指针
Array length数组的长度(如果当前对象是数组)

Mark Word里默认存储对象的HashCode、分代年龄和锁标记位,结构如下:

在这里插入图片描述

轻量级锁

如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。轻量级锁对使用者是透明的,即语法仍然是 synchronized;

假设有两个方法同步块,利用同一个对象加锁:

static final Object obj = new Object();
public static void method1() {
	 synchronized( obj ) {
	 // 同步块 A
	 method2();
	 }
}
public static void method2() {
	 synchronized( obj ) {
	 // 同步块 B
	 }
}

创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word:
在这里插入图片描述
让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录:
在这里插入图片描述
如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下:
在这里插入图片描述
如果 cas 失败,有两种情况:

  1. 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程

  2. 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数

    在这里插入图片描述

当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时删除取值为null的锁记录,表示重入计数减一;

当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头:

  • 成功,则解锁成功
  • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

锁膨胀

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁;

static Object obj = new Object();
public static void method1() {
 synchronized( obj ) {
 // 同步块
 }
}

当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁:
在这里插入图片描述
这时 Thread-1 加轻量级锁失败,进入锁膨胀流程:

  1. 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
  2. 然后自己进入 Monitor 的 EntryList BLOCKED

在这里插入图片描述
当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程;

偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程获得,为了让线程获得锁的代价更低而引入了偏向锁;

偏向锁只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有;

static final Object obj = new Object();
public static void m1() {
	 synchronized( obj ) {
	 // 同步块 A
	 m2();
	 }
}
public static void m2() {
	 synchronized( obj ) {
	 // 同步块 B
	 m3();
	 }
}
public static void m3() {
	 synchronized( obj ) {
	 // 同步块
	 }
}

流程如下:
在这里插入图片描述

偏向锁的撤销

偏向锁的撤销需要等待全局安全点(在这个时间点上没有正在执行的字节码),它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态则将对象头设置为无锁状态;

如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象中的Mark Word要么重新偏向于其他线程,要么恢复到无锁状态或者标记对象不适合作为偏向锁,最后唤醒暂停的线程;

锁粗化、锁消除

锁粗化:就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。以此来减少在锁操作上的开销。

锁消除:是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。

锁的分类

在这里插入图片描述

公平锁和非公平锁

公平指的是按照线程请求的顺序来分配锁,非公平指的是不完全按照请求的顺序,在一定的情况下(线程被唤醒的时候需要一点时间,在这个时间范围内允许其它线程插队,可以提高效率)可以插队;

非公平:避免唤醒带来的空档期

乐观锁和悲观锁

每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁,但是在更新数据的时候需要判断该数据是否被别人修改过。如果数据被其他线程修改,则不进行数据更新,如果数据没有被其他线程修改,则进行数据更新。由于数据没有进行加锁,期间该数据可以被其他线程进行读写操作

乐观锁的实现一般都是利用CAS算法来实现的;

Java中悲观锁的实现就是synchronized和Lock相关类;

乐观锁的典型例子就是原子类、并发容器;

public class Test7
{
    public static void main(String[] args) {
        //乐观锁
        AtomicInteger atomicInteger = new AtomicInteger();
        atomicInteger.incrementAndGet();
    }
    
    //悲观锁
    public synchronized void test(){
        
    }
}

数据库的select for update 是悲观锁,用version控制数据库则是乐观锁

乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。
悲观锁:比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。

共享锁(读锁)和独占锁(写锁)

数据库中的定义:

  • 共享锁:若事务T对数据A加上S锁,则事务T只能读A;其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁;
  • 独占锁:若事务T对数据对象A加上X锁,则只允许T读取和修改A,其它任何事务都不能再对A加任何类型的锁,直到T释放A上的锁;

Java中:

  • 共享锁:允许多个线程同时获取一个锁,一个锁可以同时被多个线程拥有;
  • 独占锁:一个锁在某一时刻只能被一个线程占有,其它线程必须等待锁被释放之后才可能获取到锁;

要嘛是一个线程或多个线程同时有读锁,要嘛是一个线程有写锁,但是两者不会同时出现(要么多读,要么一写);

/**
 * @author yangdong
 * @date 2021-05-19
 * 演示ReentrantReadWriteLock的使用:读读共享、写写互斥、读写互斥
 */
public class UseReentrantReadWriteLock {

    private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
    private ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();

    public void read(){
        try {
            readLock.lock();
            System.out.println("当前线程:" + Thread.currentThread().getName() + "进入读锁...");
            Thread.sleep(3000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readLock.unlock();
            System.out.println("当前线程:" + Thread.currentThread().getName() + "退出读锁...");
        }
    }

    public void write(){
        try {
            writeLock.lock();
            System.out.println("当前线程:" + Thread.currentThread().getName() + "进入写锁...");
            Thread.sleep(3000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            writeLock.unlock();
            System.out.println("当前线程:" + Thread.currentThread().getName() + "退出写锁...");
        }
    }

    public static void main(String[] args) {
        final UseReentrantReadWriteLock urrw = new UseReentrantReadWriteLock();
        new Thread(()->urrw.read()).start();
        new Thread(()->urrw.read()).start();
        new Thread(()->urrw.write()).start();
        new Thread(()->urrw.read()).start();
        new Thread(()->urrw.write()).start();
    }
}

读锁插队策略

ReentrantReadWriteLock 可以设置为公平或者非公平,代码如下:

//可以根据构造函数的入参来设置公平或者非公平。默认是非公平锁
public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}

static final class FairSync extends Sync {
    private static final long serialVersionUID = -2274990926593161451L;
    final boolean writerShouldBlock() {return hasQueuedPredecessors();}
    final boolean readerShouldBlock() {return hasQueuedPredecessors();}
}

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = -8159625535654395037L;
    final boolean writerShouldBlock() {return false; // writers can always barge}
    final boolean readerShouldBlock() {return apparentlyFirstQueuedIsExclusive();}
}

公平锁不允许插队。非公平锁写锁可以随便插队,读锁仅在等待队列头结点不是想获取写锁的线程的时候可以插队;

读锁如果可以随便插队的话会产生一个很严重的问题,那就是如果想要读取的线程不停地增加从而导致读锁长时间内不会被释放,这样就会导致写入线程长时间内拿不到写锁,也就是说写入线程会陷入“饥饿”状态,它将在长时间内得不到执行;
在这里插入图片描述
所以我们可以看出,即便是非公平锁,只要等待队列的头结点是尝试获取写锁的线程,那么读锁依然是不能插队的,目的是避免“饥饿”。

ReentrantReadWriteLock 的实现选择了“不允许插队”的策略;

锁的升降级

读锁不可以升级为写锁,若两个线程的读锁都想升级写锁,则需要对方都释放自己锁(因为读写互斥),而双方都不释放,就会产生死锁;

写锁是可以降级为读锁的,因为写锁只有一个,当写锁降级为读锁时,所有的都是读;

/**
 * @author yangdong
 * @date 2021-05-20
 * 演示写锁的降级
 */
public class ReadWriteDowngradeDemo {
    private static ReentrantReadWriteLock readWriteLock=new ReentrantReadWriteLock(false);
    private static ReentrantReadWriteLock.ReadLock readLock=readWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock=readWriteLock.writeLock();

    public void downgrade(){
        writeLock.lock();
        try{
            System.out.println(Thread.currentThread().getName()+"获取写锁");
            readLock.lock();
            try{
                System.out.println(Thread.currentThread().getName()+"降级获取读锁");
            }finally {
                readLock.unlock();
            }
        }finally {
            writeLock.unlock();
        }
    }

    public static void main(String[] args) {
        ReadWriteDowngradeDemo readWriteUpgardeDemo=new ReadWriteDowngradeDemo();
        new Thread(()->readWriteUpgardeDemo.downgrade()).start();
    }
}
//控制台打印:
Thread-0获取写锁
Thread-0降级获取读锁

替换获取锁的顺序:先获取读锁再获取写锁;再次运行,程序会进入阻塞,这也说明了读锁无法升级为写锁;

自旋锁、非自旋锁、自适应自旋锁

在这里插入图片描述
在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋。

原图在这:https://blog.csdn.net/weixin_38106322/article/details/105595087

Synchronized

1、同步代码块

public void draw(){
	//使用object作为同步监视器,任何线程进入下面同步代码块之前,必须先获得对object的锁定——其他线程无法获得锁,也就无法修改它
	//这种做法符合:加锁-->修改完成-->释放锁 逻辑
	synchronized (object){
	}
}

synchronized后括号里的obj就是同步监视器,任何时刻只能有一个线程可以获得同步监视器的锁定。当同步代码块执行完成后,该线程会释放对该同步监视器的锁定;

通常推荐使用可能被并发访问的共享资源充当同步监视器;

使用synchronized(this)时需要注意:当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对同一个object中所有其他synchronized(this)同步代码块的访问将被阻塞(因为this表示当前对象,说明使用的对象监视器是同一个,即锁是同一个)。

2、同步方法

同步方法就是使用synchronized关键字来修饰某个方法,则该方法称为同步方法(用synchronized 声明方法时,将其放在public之前或之后没有区别)。对于synchronized修饰的实例方法(非static方法)而言,无须显式指定同步监视器,同步方法的同步监视器是this,也就是调用该方法的对象;

//synchronized 修饰,把该方法变成同步方法,该同步方法的同步监视器是this
public synchronized void draw(){

}

synchronized 关键字可以修饰方法或者代码块,但是不能修饰构造器、成员变量等!

在方法声明处添加synchronized 并不是锁方法,而是锁当前的对象,哪个线程拿到这把锁,哪个线程就可以执行这个对象中的synchronized 同步方法:

class MyService1{
    synchronized public void methodA(){
        try {
            System.out.println("methodA");
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public void methodB(){
        try {
            System.out.println("methodB");
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    synchronized public void methodC(){
        try {
            System.out.println("methodC");
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class TestSync{
    public static void main(String[] args) {
        MyService1 service1 = new MyService1();
        new Thread(()->{service1.methodA();}).start();
        new Thread(()->{service1.methodB();}).start();
        new Thread(()->{service1.methodC();}).start();
    }
}
//控制台打印
methodA
methodB

虽然线程A先持有了MyService1 对象的锁,但线程B完全可以异步调用非synchronized 类型的方法!

3、synchronized原理

synchronized public static void testMethod(){}转为字节码指令后为:
public static synchronized void testMethod();
  descriptor: ()V
  flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
  Code:
    stack=0, locals=0, args_size=0
       0: return
    LineNumberTable:
      line 9: 0

在方法中使用synchronized 关键字实现同步的原理是flag标记ACC_SYNCHRONIZED,当调用方法时,调用指令会检查方法的ACC_SYNCHRONIZED访问标志是否设置,如果设置了,执行线程先持有同步锁,然后执行方法,最后在方法完成时释放锁。

public void testMethod(){
    synchronized (this){
    }
}转为字节码指令后为:
public void testMethod();
  descriptor: ()V
  flags: ACC_PUBLIC
  Code:
    stack=2, locals=3, args_size=1
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter
       4: aload_1
       5: monitorexit
       6: goto          14
       9: astore_2
      10: aload_1
      11: monitorexit
      12: aload_2
      13: athrow
      14: return

使用synchronized 代码块,则使用monitorenter和monitorexit指令进行同步处理!

每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针,monitor结构如下:
在这里插入图片描述
流程如下:

  1. 刚开始 Monitor 中 Owner 为 null
  2. 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner
  3. 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入EntryList BLOCKED
  4. Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争是非公平的

图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程;

synchronized 必须是进入同一个对象的 monitor 才有上述的效果
不加 synchronized 的对象不会关联监视器,不遵从以上规则

4、synchronized 具有可重入特性

可重入锁是指自己可以再次获取自己的内部锁。例如,一个线程获得了某个对象锁,此时这个对象锁还没有释放,当其再次想要获取这个对象锁时还是可以获取的:

class MyService2{
    synchronized public void method1(){
        System.out.println("method1");
        method2();
    }
    synchronized public void method2(){
        System.out.println("method2");
        method3();
    }
    synchronized public void method3(){
        System.out.println("method3");
    }
}
public class Test6 {
    public static void main(String[] args) {
        MyService2 service = new MyService2();
        new Thread(()->{service.method1();}).start();
    }
}
//控制台打印
method1
method2
method3

结果表明,如果不是可重入锁,则method2()、method3()不会被调用!

synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁,在执行完同步代码块时,计数器的数量会-1,直到计数器的数量为0,就释放这个锁;

另外,当存在继承关系时,子类是完全可以通过锁重入调用父类的同步方法的(重写父类方法如果不使用synchronized 关键字,即是非同步方法):

class MyService2{
    synchronized public void superMethod(){
        System.out.println("super");
    }
}
public class Test6 extends MyService2{
    synchronized public void subMethod(){
        System.out.println("sub");
        this.superMethod();
    }
    public static void main(String[] args) {
        new Thread(()->{new Test6().subMethod();}).start();
    }
}
//控制台打印
sub
super

因为子类继承了父类的同步方法。调用父类的同步方法其实就是调用本身的同步方法!

5、synchronized 出现异常,锁会自动释放

当线程执行的代码出现异常时,其持有的锁会自动释放:

public class Test6{
    synchronized public void test(){
        try {
            System.out.println("线程"+Thread.currentThread().getName()+"获取到锁!");
            Thread.sleep(5000);
            if (Thread.currentThread().getName().equals("A")){
                throw new Exception();
            }
        } catch (Exception e) {
            System.out.println("线程:"+Thread.currentThread().getName()+" 抛出异常!");
        }
    }
    public static void main(String[] args) {
        Test6 test6 = new Test6();
        Thread threadA = new Thread(() -> { test6.test(); });
        Thread threadB = new Thread(() -> { test6.test(); });
        threadA.setName("A");
        threadB.setName("B");
        threadA.start();
        threadB.start();
    }
}
//控制台打印
线程A获取到锁!
线程:A 抛出异常!
线程B获取到锁!

6、静态synchronized方法与synchronized(class)代码块

使用静态synchronized方法或synchronized(class)代码块效果是一样的,都是使用对应Class类的单例对象作为锁(Class对象作为锁,对类的所有对象实例起作用)!

synchronized关键字加到非static方法上是将方法所在类的对象作为锁!

public class Test6 {
    synchronized public static void method(){
        try {
            System.out.println("method");
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void method2(){
        synchronized (Test6.class){
            System.out.println("method2");
        }
    }
    public static void main(String[] args) {
        new Thread(()->{Test6.method();}).start();
        new Thread(()->{Test6.method2();}).start();
    }
}

每一个*.java文件对应Class类的实例都是一个,在内存中是单例的:

public class Test6 {
    public static void main(String[] args) {
        Test6 test1 = new Test6();
        Test6 test2 = new Test6();
        //输出true
        System.out.println(test1.getClass() == test2.getClass());
    }
}

Class类用于描述类的基本信息,为了减少对内存的高占用率,在内存中只需要存在一份Class对象就可以了,所以被设计为单例的!

7、不可中断

一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断;

/**
 * @author yangdong
 * @date 2021-05-19
 * 演示synchronized不可中断
 */
public class Demo02_Uninterruptible{
    private static Object obj = new Object();
    public static void main(String[] args) throws InterruptedException {
        Runnable run = () -> {
            synchronized (obj) {
                System.out.println(Thread.currentThread().getName() + "进入同步代码块");
                try {
                    //休眠一百秒,让线程2处于BLOCKED状态
                    Thread.sleep(100000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread t1 = new Thread(run);
        t1.start();

        //主线程休眠1秒,使用线程2来中断
        Thread.sleep(1000);

        Thread t2 = new Thread(run);
        t2.start();
        t2.interrupt();

        System.out.println(t1.getState());
        System.out.println(t2.getState());
    }
}
//控制台打印
Thread-0进入同步代码块
TIMED_WAITING
RUNNABLE

Volatile

volatile是保证共享变量的可见性(当一个线程修改一个共享变量时,另一个线程能立即读到这个修改的值)。

private volatile boolean flag ;

原理如下:
在这里插入图片描述

总结: volatile保证不同线程对共享变量操作的可见性,也就是说一个线程修改了volatile修饰的变量,当修改写回主内存时,另外一个线程立即看到最新的值!

volatile除了可以保证可见性外,volatile还具备如下一些突出的特性:

  1. volatile不能保证原子性操作
  2. volatile可以防止指令重排序操作

volatile不保证原子性:

所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。volatile不保证原子性。

public class VolatileAtomicThread implements Runnable{
	// 定义一个int类型的遍历
	private int count = 0 ;
	
	@Override
	public void run() {
		// 对该变量进行++操作,100次
		for(int x = 0 ; x < 100 ; x++) {
			count++ ;
			System.out.println("count =========>>>> " + count);
		}
	}
}

public class VolatileAtomicThreadDemo {

	public static void main(String[] args) {
		// 创建VolatileAtomicThread对象
		VolatileAtomicThread volatileAtomicThread = new VolatileAtomicThread() ;
		
		// 开启100个线程对count进行++操作
		for(int x = 0 ; x < 100 ; x++) {
			new Thread(volatileAtomicThread).start();
		}
	}
}

执行结果:不保证一定是10000

以上问题主要是发生在count++操作上:

count++操作包含3个步骤:

  1. 从主内存中读取数据到工作内存
  2. 对工作内存中的数据进行++操作
  3. 将工作内存中的数据写回到主内存

count++操作不是一个原子性操作,也就是说在某一个时刻对某一个操作的执行,有可能被其他的线程打断
在这里插入图片描述
流程:

  1. 假设此时x的值是100,线程A需要对改变量进行自增1的操作,首先它需要从主内存中读取变量x的值。由于CPU的切换关系,此时CPU的执行权被切换到了B线程。A线程就处于就绪状态,B线程处于运行状态
  2. 线程B也需要从主内存中读取x变量的值,由于线程A没有对x值做任何修改因此此时B读取到的数据还是100
  3. 线程B工作内存中x执行了+1操作,但是未刷新之主内存中
  4. 此时CPU的执行权切换到了A线程上,由于此时线程B没有将工作内存中的数据刷新到主内存,因此A线程工作内存中的变量值还是100,没有失效,A线程对工作内存中的数据进行了+1操作
  5. 线程B将101写入到主内存
  6. 线程A将101写入到主内存

虽然计算了2次,但是只对A进行了1次修改;

总结:在多线程环境下,volatile关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性(在多线程环境
下volatile修饰的变量也是线程不安全的)。
在多线程环境下,要保证数据的安全性,我们还需要使用锁机制

使用锁机制:
我们可以给count++操作添加锁,那么count++操作就是临界区的代码,临界区只能有一个线程去执行,所以count++就变成了原子操作。

public class VolatileAtomicThread implements Runnable{
	// 定义一个int类型的变量
	private volatile int count = 0 ;
	private static final Object obj = new Object();
	
	@Override
	public void run() {
		// 对该变量进行++操作,100次
		for(int x = 0 ; x < 100 ; x++) {
			synchronized (obj) {
				count++ ;
				System.out.println("count =========>>>> " + count);
			}
		}
	}
}

观察控制台会发现结论始终会是10000!

禁止指令重排序:
什么是重排序:为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序。

原因:一个好的内存模型实际上会放松对处理器和编译器规则的束缚,也就是说软件技术和硬件技术都为同一个目标而进行奋斗:在不改变程序执行结果的前提下,尽可能提高执行效率。JMM对底层尽量减少约束,使其能够发挥自身优势。因此,在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排序。一般重排序可以分为如下三种:

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
  3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的

在这里插入图片描述
重排序的好处:重排序可以提高处理的速度!
在这里插入图片描述
重排序虽然可以提高执行的效率,但是在并发执行下,JVM虚拟机底层并不能保证重排序下带来的安全性等问题!

volatile修饰变量后可以实现禁止指令重排序!

volatile与synchronized的区别

  • volatile只能修饰实例变量和类变量,而synchronized可以修饰方法,以及代码块
  • volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全);而synchronized是一种排他(互斥)的机制
  • volatile用于禁止指令重排序:可以解决单例双重检查对象初始化代码执行乱序问题
  • volatile可以看做是轻量版的synchronized,volatile不保证原子性,但是如果是对一个共享变量进行多个线程的赋值,而没有其他的操作,那么就可以用volatile来代替synchronized,因为赋值本身是有原子性的,而volatile又保证了可见性,所以就可以保证线程安全了

定时器Timer

Timer类的主要任务是设置计划任务,即在指定时间开始执行某一个任务!

TimerTask类的主要作用是封装任务!

schedule(TimerTask task,Date time)、schedule(TimerTask task,Date firstTime,long period)

schedule(TimerTask task,Date time):在指定日期执行一次某一任务:
schedule(TimerTask task,Date firstTime,long period):在指定日期之后按指定的间隔周期无限循环地执行某一任务;

第二个参数早于当前时间则为立即执行!

class MyTask extends TimerTask{
    @Override
    public void run() {
        System.out.println("+++++++");
    }
}
public class Test6 extends TimerTask {
    public static void main(String[] args) throws InterruptedException {
        Timer timer = new Timer();
        //执行时间早于当前时间则立即执行
        //timer.schedule(new Test6(),new Date(System.currentTimeMillis()-3000));
        //3s后执行任务,打印语句
        timer.schedule(new Test6(),new Date(System.currentTimeMillis()+3000));
        //同时执行多个任务
        timer.schedule(new MyTask(),new Date(System.currentTimeMillis()+3000));
        //5s后执行一次,然后按5s的间隔无限执行下去
        timer.schedule(new MyTask(),new Date(System.currentTimeMillis()+5000),5000);
    }
    @Override
    public void run() {
        System.out.println("------");
    }
}

3s后任务成功执行,但是进程并未销毁,说明内部还有非守护线程正在执行;这是因为在创建Timer对象时启动了一个新的非守护线程,并且使用while(true)死循环一直执行计划任务:

	private final TimerThread thread = new TimerThread(queue);
    public Timer() {
        this("Timer-" + serialNumber());
    }
    public Timer(String name) {
        thread.setName(name);
        thread.start();
    }    
    private void mainLoop() {
        while (true) {
            try {
                TimerTask task;
                boolean taskFired;
                synchronized(queue) {
                    // Wait for queue to become non-empty
                    while (queue.isEmpty() && newTasksMayBeScheduled)
                        queue.wait();
                    if (queue.isEmpty())
                        break; // Queue is empty and will forever remain; die

不执行public void cancel(),newTasksMayBeScheduled就会永远为true,进程就会一直死循环状态;

public void cancel()

它的作用是终止此计时器,丢弃所有当前已安排的任务,这并不会干扰当前正在执行的任务;

public class Test6 extends TimerTask {
    public static void main(String[] args) throws InterruptedException {
        Timer timer = new Timer();
        timer.schedule(new Test6(),new Date(System.currentTimeMillis()+3000));
        Thread.sleep(3000);
        timer.cancel();
    }
    @Override
    public void run() {
        System.out.println("------");
    }
}

schedule(TimerTask task,long delay)、schedule(TimerTask task,long delay,long period)

schedule(TimerTask task,long delay):延迟指定的毫秒数执行一次TimerTask任务;

schedule(TimerTask task,long delay,long period):延迟指定的毫秒数执行一次TimerTask任务,再以指定的间隔时间无限执行任务;

public class Test6 extends TimerTask {
    public static void main(String[] args) throws InterruptedException {
        Timer timer = new Timer();
        //3s后执行一次
        //timer.schedule(new Test6(),3000);	
        //3s后执行一次,然后每隔3s都执行一次
        timer.schedule(new Test6(),3000,3000);
    }
    @Override
    public void run() {
        System.out.println("------");
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值