栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

线程安全、锁、并发相关类

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

线程安全、锁、并发相关类

多线程理解(一)
多线程理解(二)
JVM内存结构、Java内存模型、Java对象模型

线程安全

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

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

线程安全示例: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、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());
    }
}

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

锁是一种工具,用于控制对共享资源的访问;

Lock和synchronized,这两个是最常见的锁,它们都可以达到线程安全的目的,但是在使用上和功能上又有较大的不同;

Lock并不是用来提代synchronized,而是当使用synchronized不合适或不满足要求的时候,来提供高级功能的;

为什么synchronized不够用?

    效率低:锁的释放情况少,视图获得锁时不能设定超时、不能中断一个正在视图获取锁的线程;不够灵活:加锁和释放的时机单一,每个锁仅有单一的条件;
锁的分类

公平锁和非公平锁

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

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

乐观锁和悲观锁

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

乐观锁的实现一般都是利用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中:

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

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

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 的实现选择了“不允许插队”的策略;

锁的升降级

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

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

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降级获取读锁

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

并发工具类 synchronized同步方法/代码块

1、同步代码块

为了解决这些问题,Java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块:

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、释放同步监视器的锁定

4、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指令进行同步处理!

monitorenter

每一个对象都会和一个监视器monitor关联,监视器被占用时会被锁住,其他线程无法来获取该monitor。 当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应的monitor的所有权。其过程如下:

    若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为monitor的owner(所有者);若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1;若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直到monitor的进入数变为0,才能重新尝试获取monitor的所有权;

synchronized的锁对象会关联一个monitor,这个monitor不是我们主动创建的,是JVM的线程执行到这个同步代码块,发现锁对象没有monitor就会创建monitor,monitor内部有两个重要的成员变量:

    owner:拥有这把锁的线程recursions:记录线程拥有锁的次数

当一个线程拥有monitor后其他线程只能等待;

monitorexit

能执行monitorexit指令的线程一定是拥有当前对象 monitor 的所有权的线程,执行monitorexit时会将 monitor 的进入数减1。当 monitor 的进入数减为0时,当前线程退出 monitor,不再拥有 monitor 的所有权,此时其他被这个 monitor 阻塞的线程可以尝试去获取这个 monitor 的所有权;

monitorexit插入在方法结束处和异常处,JVM保证每个monitorenter必须有对应的monitorexit。

monitor监视器锁

每一个Java对象都可以与一个监视器monitor关联,我们可以把它理解成为一把锁,当一个线程想要执行一段被synchronized圈起来的同步方法或者代码块时,该线程得先获取到synchronized修饰的对象对应的monitor;

monitor并不是随着对象创建而创建的。我们是通过synchronized修饰符告诉JVM需要为我们的某个对象创建关联的monitor对象;

5、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

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

6、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获取到锁!

7、静态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对象就可以了,所以被设计为单例的!

8、不可中断

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

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

9、holdsLock()方法

public static boolen holdsLock(Object obj)方法的作用是当currentThread在指定的对象上保持锁定时才返回true:

public class Test6 {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().holdsLock(Test6.class));
        synchronized (Test6.class){
            System.out.println(Thread.currentThread().holdsLock(Test6.class));
        }
        System.out.println(Thread.currentThread().holdsLock(Test6.class));
    }
}
//控制台打印
false
true
false
volatile

volatile关键字修饰:

private volatile boolean flag ;

    子线程t从主内存读取到数据放入其对应的工作内存将flag的值更改为true,但是这个时候flag的值还没有写会主内存此时main方法main方法读取到了flag的值为false当子线程t将flag的值写回去后,失效其他线程对此变量副本再次对flag进行操作的时候线程会从主内存读取最新的值,放入到工作内存中

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

volatile的其他特性

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

    volatile不能保证原子性操作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个步骤:

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

count++操作不是一个原子性操作,也就是说在某一个时刻对某一个操作的执行,有可能被其他的线程打断

流程:

    假设此时x的值是100,线程A需要对改变量进行自增1的操作,首先它需要从主内存中读取变量x的值。由于CPU的切换关系,此时CPU的执行权被切换到了B线程。A线程就处于就绪状态,B线程处于运行状态线程B也需要从主内存中读取x变量的值,由于线程A没有对x值做任何修改因此此时B读取到的数据还是100线程B工作内存中x执行了+1操作,但是未刷新之主内存中此时CPU的执行权切换到了A线程上,由于此时线程B没有将工作内存中的数据刷新到主内存,因此A线程工作内存中的变量值还是100,没有失效,A线程对工作内存中的数据进行了+1操作线程B将101写入到主内存线程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对底层尽量减少约束,使其能够发挥自身优势。因此,在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排序。一般重排序可以分为如下三种:

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


重排序的好处:重排序可以提高处理的速度!

重排序虽然可以提高执行的效率,但是在并发执行下,JVM虚拟机底层并不能保证重排序下带来的安全性等问题!

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

volatile与synchronized的区别

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

在Lock中声明了四个方法来获取锁:

lock()unlock()tryLock()tryLock(long time, TimeUnit unit)lockInterruptibly()

1、lock()、unlock()

lock():获取锁,如果锁已被其它线程获取,则进行等待;

unlock():释放锁;

public class Test7
{
    private static Lock lock = new ReentrantLock();
    public static void main(String[] args) {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"开始执行任务");
        }finally {
            lock.unlock();
        }
    }
}

Lock不会像synchronized一样在异常时自动释放锁。因此我们一定要在 finally 中释放锁,以保证发生异常时锁一定被释放;

public class Test7
{
    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        lock.lock();
        try {
            throw new RuntimeException();
        }catch (Exception e){
            e.printStackTrace();
        }
        //isHeldByCurrentThread可以看出锁是否被当前线程持有
        System.out.println(lock.isHeldByCurrentThread());
    }
}

运行程序,控制台打印true,证实:Lock发生异常不会自动释放锁;

lock()方法不能被中断,这就会带来很大的问题:一旦陷入死锁,lock()就会陷入永久等待;

2、tryLock()、tryLock(long time, TimeUnit unit),嗅探拿锁

tryLock()用来尝试获取锁,该方法会立即返回,即便在拿不到锁时也不会一直等待,它的返回值是一个布尔值(返回true代表获取成功,返回false代表获取失败);

tryLock(long time, TimeUnit unit)则会进行一段时间等待,如果在此等待时间内拿到了锁则返回true,否则返回false;

相比于lock(),这个方法显然功能更强大了,我们可以根据是否能获取刀锁来决定后续程序的行为;

public class Test7 implements Runnable
{
    private static ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        if (lock.tryLock()){
            try {
                System.out.println(Thread.currentThread().getName()+"使用tryLock()获取到锁");
                if(lock.tryLock(100,TimeUnit.MILLISECONDS)){
                    System.out.println(Thread.currentThread().getName()+"使用tryLock(long time, TimeUnit unit)获取到锁");
                }
                //打印 ReentrantLock 的嵌套加锁数量
                System.out.println(lock.getHoldCount());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
            	//必须显式调用 unlock() 来释放锁
                lock.unlock();
                lock.unlock();
                System.out.println(lock.getHoldCount());
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new Test7()).start();
    }
}
//控制台打印:
Thread-0使用tryLock()获取到锁
Thread-0使用tryLock(long time, TimeUnit unit)获取到锁
2
0

ReentrantLock 具有可重入性,一个线程可以对已被加锁的 ReentrantLock 再次加锁,ReentrantLock 对象会维持一个计数器来追踪 lock() 方法的嵌套调用,线程在每次调用 lock() 加锁后,必须显式调用 unlock() 来释放锁;

3、lockInterruptibly()

相当于tryLock(long time, TimeUnit unit)把超时时间设置为无限。在等待锁的过程中,线程可以被中断,不会抛出InterruptedException异常;

public class Test7 implements Runnable
{
    private static ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        try {
            lock.lockInterruptibly();
            try {
                System.out.println(Thread.currentThread().getName()+"获取到锁");
                Thread.sleep(1000);
            }finally {
                lock.unlock();
                System.out.println(Thread.currentThread().getName()+"释放了锁");
            }
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName()+"等待获取锁时被中断了");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new Test7());
        Thread thread2 = new Thread(new Test7());
        thread1.start();
        Thread.sleep(50);
        thread2.start();
        thread2.interrupt();
    }
}

4、ReentrantLock公平锁演示

public class FairLock implements Runnable{
    // 用true初始化锁就得到公平锁
    private Lock lock = new ReentrantLock(true);

    @Override
    public void run() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"获取到锁");
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        new Thread(new FairLock()).start();
        new Thread(new FairLock()).start();
        new Thread(new FairLock()).start();
    }
}
//控制台打印
Thread-0获取到锁
Thread-1获取到锁
Thread-2获取到锁

5、getHoldCount()、isFair()、isHeldByCurrentThread()、isLocked()

public int getHoldCount():查询当前线程保持此锁的个数(即调用lock()方法的次数,此方法在ReentrantLock 类中)!

public final boolean isFair():判断是不是公平锁(默认情况下,ReentrantLock 使用的非公平锁)!

public boolean isHeldByCurrentThread():查询当前线程是否持有此锁!

public boolean isLocked():查询此锁是否由任意线程持有!

public class Test6{
    public static void main(String[] args) throws InterruptedException, NoSuchFieldException, IllegalAccessException {
        ReentrantLock lock = new ReentrantLock();
        new Thread(()->{
            try {
            	System.out.println(lock.isLocked());	//打印false
            	System.out.println(lock.isHeldByCurrentThread());	//打印false
                lock.lock();
                System.out.println(lock.isLocked());	//打印true
                System.out.println(lock.isHeldByCurrentThread());	//打印true
                System.out.println(lock.getHoldCount());			//打印1
            }finally {
                lock.unlock();
                System.out.println(lock.getHoldCount());			//打印0
            }
        }).start();
    }
    System.out.println(lock.isFair());		//打印false
}

6、getQueueLength()

public final int getQueueLength()方法的作用是返回正在等待获取此锁的线程数量!

public class Test6{
    static ReentrantLock lock = new ReentrantLock();
    public static void method(){
        try {
            lock.lock();
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) throws InterruptedException, NoSuchFieldException, IllegalAccessException {
        for (int i = 0; i < 5; i++) {
            new Thread(()->{ method();}).start();
        }
        Thread.sleep(50);
        System.out.println(lock.getQueueLength());	//打印4
    }
}

7、hasWaiters(Condition condition)、getWaitQueueLength(Condition condition)

public boolen hasWaiters(Condition condition)方法的作用是查询是否有线程正在等待与此锁有关的Condition条件(是否有线程执行了condition对象中的await()方法而呈等待状态)!

public int getWaitQueueLength(Condition condition) 方法的作用是返回等待与此锁相关的给定条件Condition的线程数量!

public class Test6{
    static ReentrantLock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();
    public static void method(){
        try {
            lock.lock();
            condition.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public static void signal(){
        try {
            lock.lock();
            System.out.println(lock.getWaitQueueLength(condition));	//打印5
            System.out.println(lock.hasWaiters(condition));			//打印true
            condition.signal();
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException, NoSuchFieldException, IllegalAccessException {
        for (int i = 0; i < 5; i++) {
            new Thread(()->{ method();}).start();
        }
        Thread.sleep(1000);
        signal();
    }
}

8、hasQueuedThread(Thread thread)、hasQueuedThreads()

public final boolean hasQueuedThread(Thread thread)方法的作用是查询指定的线程是否正在等待获取此锁,也就是判断参数中的线程是否在等待队列中;

public final boolean hasQueuedThreads()方法的作用是查询是否有线程正在等待获取此锁,也就是等待队列中是否有等待的线程!

public class Test6{
    static ReentrantLock lock = new ReentrantLock();
    public static void method(){
        try {
            lock.lock();
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException, NoSuchFieldException, IllegalAccessException {
        new Thread(()->{ method();}).start();
        Thread.sleep(50);
        Thread thread = new Thread(() -> { method(); });
        thread.start();
        Thread.sleep(50);
        System.out.println(lock.hasQueuedThread(thread));	//打印true
        System.out.println(lock.hasQueuedThreads());		//打印true
    }
}

自旋锁和阻塞锁

阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间

如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长

在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失

public class SpinLockDemo implements Runnable{
    private static AtomicReference atomicReference=new AtomicReference<>();
    public void lock(){
        while(!atomicReference.compareAndSet(null,Thread.currentThread())){
            System.out.println(Thread.currentThread().getName()+"自旋锁获取失败,重新获取中");
        }
    }
    public void unlock(){
        atomicReference.compareAndSet(Thread.currentThread(),null);
    }

    public static void main(String[] args) {
        new Thread(new SpinLockDemo()).start();
        new Thread(new SpinLockDemo()).start();
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"尝试获取自旋锁");
        lock();
        System.out.println(Thread.currentThread().getName()+"获取自旋锁成功");
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        unlock();
        System.out.println(Thread.currentThread().getName()+"释放自旋锁");
    }
}
Thread-0尝试获取自旋锁
Thread-0获取自旋锁成功
Thread-1尝试获取自旋锁
Thread-1自旋锁获取失败,重新获取中
Thread-1自旋锁获取失败,重新获取中
Thread-1自旋锁获取失败,重新获取中
Thread-1自旋锁获取失败,重新获取中
Thread-0释放自旋锁
Thread-1获取自旋锁成功
Thread-1释放自旋锁

如果锁被占用的时间很长,那么自旋的线程只会白白浪费处理器的资源;

原子类

在java.util.concurrent.atomic包下,有一系列Atomic开头的类,统称为原子类;

原子类的作用和锁类似,都是为了保证并发情况下线程安全,不过原子类相比于锁,有以下两大优势:

粒度更细效率更高

原子类总共分为以下六种:

分类具体
Atomic*基本类型原子类AtomicInteger、AtomicLong、AtomicBoolean
Atomic*Array数组类型原子类AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
Atomic*Reference引用类型原子类AtomicReference、AtomicStampedReference、AtomicMarkableReference
Adder累加器LongAdder、DoubleAdder
Accumulator累加器LongAccumulator、DoubleAccumulator

AtomicInteger

//获取当前的值
public final int get()

//获取当前的值,并设置新的值
public final int getAndSet(int newValue)

//获取当前的值,并自增
public final int getAndIncrement()

//获取当前的值,并自减
public final int getAndDecrement()

//获取当前的值,并加上预取的值
public final int getAndAdd(int delta)

//如果输入的数值等于预期值,则以原子方式将该值设置为输入值
public final boolean weakCompareAndSet(int expect, int update)
public class AtomicIntegerDemo implements Runnable{

    private static final AtomicInteger atomicInteger = new AtomicInteger();
    private static volatile  int basicCount = 0;

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            atomicInteger.getAndIncrement();
            basicCount++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicIntegerDemo demo = new AtomicIntegerDemo();
        Thread t1 = new Thread(demo);
        Thread t2 = new Thread(demo);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("原子类的结果:"+atomicInteger.get());
        System.out.println("普通变量的结果:"+basicCount);
    }
}

多次运行发现原子类的操作总能返回正确的结果,而非原子类则不行;

AtomicIntegerArray

以整个数组对象为单位,里面的元素操作都是原子性的;

public class AtomicArrayDemo implements Runnable{
    static AtomicIntegerArray array = new AtomicIntegerArray(100);
    public static void main(String[] args) throws InterruptedException {
        AtomicArrayDemo demo = new AtomicArrayDemo();
        Thread t1 = new Thread(demo);
        Thread t2 = new Thread(demo);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }

    @Override
    public void run() {
        for (int i = 0; i < array.length(); i++) {
            array.getAndIncrement(i);
        }
    }
}

运行程序,因为AtomicIntegerArray属于原子类数组,因此在多线程情况下,数组里面的元素操作都是线程安全的,不会出现结果不正确的情况;

AtomicReference

AtomicReference类的作用和AtomicInteger类似,AtomicInteger可以让一个整数保证原子性,而AtomicReference可以让一个对象保证原子性;

@Data
@AllArgsConstructor
public class User {
    private String name;
    private Integer age;
}
public static void main( String[] args ) {
    User user1 = new User("张三", 23);
    User user2 = new User("李四", 25);
    User user3 = new User("王五", 20);

	//初始化为 user1
    AtomicReference atomicReference = new AtomicReference<>();
    atomicReference.set(user1);

	//把 user2 赋给 atomicReference
    atomicReference.compareAndSet(user1, user2);
    System.out.println(atomicReference.get());

	//把 user3 赋给 atomicReference
    atomicReference.compareAndSet(user1, user3);
    System.out.println(atomicReference.get());
}

输出结果如下:

User(name=李四, age=25)
User(name=李四, age=25)

atomicReference的初始值是user1,所以调用compareAndSet(user1, user2),由于user1==user1,所以会把user2赋给atomicReference。此时值为“李四”;

第二次调用atomicReference.compareAndSet(user1, user3),由于user2 != user1,所以set失败。atomicReference仍然为“李四”

AtomicReferenceFieldUpdater对普通变量进行升级

public class AtomicReferenceFieldUpdaterDemo implements Runnable{
    static Candidate tom;
    static Candidate peter;

    public static AtomicIntegerFieldUpdater scoreUpdate = AtomicIntegerFieldUpdater.newUpdater(Candidate.class,"score");

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            peter.score++;
            scoreUpdate.getAndIncrement(tom);
        }
    }

    public static class Candidate{
        volatile int score;
    }

    public static void main(String[] args) throws InterruptedException {
        tom = new Candidate();
        peter = new Candidate();
        AtomicReferenceFieldUpdaterDemo demo = new AtomicReferenceFieldUpdaterDemo();
        Thread t1 = new Thread(demo);
        Thread t2 = new Thread(demo);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("升级后的结果:"+tom.score);
        System.out.println("普通变量的结果:"+peter.score);
    }
}

Adder累加器

Adder是 Java 8 引入的,在高并发下LongAdder 比 AtomicLong效率高,不过本质是空间换时间;

public class AtomicLongDemo{
    public static void main(String[] args) throws InterruptedException {
        AtomicLong counter = new AtomicLong(0);
        ExecutorService service = Executors.newFixedThreadPool(20);
        long start = System.currentTimeMillis();

        for (int i = 0; i < 1000; i++) {
            service.submit(() -> {
                for (int j = 0; j < 10000; j++) {
                    counter.incrementAndGet();
                }
            });
        }
        service.shutdown();
        while (!service.isTerminated()){
        }
        long end = System.currentTimeMillis();
        System.out.println(counter.get());
        System.out.println("AtomicLong耗时:"+(end-start));
    }
}

再创建一个类,将AtomicLong 更改为LongAdder,运行程序对比测试,可以发现LongAdder的运行时间更快;

CAS CountDownLatch

CountDownLatch是在java1.5被引入,跟它一起被引入的工具类还有CyclicBarrier、Semaphore、ConcurrentHashMap和BlockingQueue。都存在于java.util.cucurrent包下。

CountDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。它是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。

CountDownLatch类中只提供了一个构造器:

//参数count为计数值
public CountDownLatch(int count) {  };  

类中有三个方法是最重要的:

//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException { };   
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  
//将count值减1
public void countDown() { };  

示例:

public class CountDownLatchDemo{
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(5);
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            final int no = i + 1 ;
            Runnable runnable = () -> {
                try {
                    long time = (long) (Math.random()*10000);
                    Thread.sleep(time);
                    System.out.println("No."+no+"完成了检查");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    latch.countDown();
                }
            };
            executorService.submit(runnable);
        }
        System.out.println("等待五个人检查完成!");
        latch.await();
        System.out.println("全部完成");
    }
}
//控制台打印:
等待五个人检查完成!
No.1完成了检查
No.3完成了检查
No.4完成了检查
No.2完成了检查
No.5完成了检查
全部完成
CyclicBarrier循环栅栏

CyclicBarrier可以构造一个集结点,当某一个线程执行完毕,它就会到集结点等待,直到所有线程都到了集结点,那么该栅栏就被册小,所有的线程再同一出发,继续执行剩下的任务;

生活中小例子:所有人都在山脚下集合,都到齐后再一起爬山;

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(5, new Runnable() {
            @Override
            public void run() {
                System.out.println("所有人都到场了,大家一起出发");
            }
        });
        for (int i = 0; i < 5; i++) {
            new Thread(new Task(i,cyclicBarrier)).start();
        }
    }


    static class Task implements Runnable{
        private int id;
        private CyclicBarrier cyclicBarrier;
        public Task(int id,CyclicBarrier cyclicBarrier){
            this.id = id;
            this.cyclicBarrier = cyclicBarrier;
        }
        @Override
        public void run() {
            System.out.println("线程"+id+"现在前往集合地点");
            try {
                Thread.sleep((long)(Math.random()*10000));
                System.out.println("到了集合地点,开始等待其他人到达");
                cyclicBarrier.await();
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }
}

CyclicBarrier和CountDownLatch的区别:

作用不同:CyclicBarrier要等固定数量的线程都到达了栅栏位置才能继续执行,而CountDownLatch只需等待数字到0,也就是说,CountDownLatch用于事件,但是CyclicBarrier是用于线程的;可重用性不同:CountDownLatch在倒数到0并出发门闩打开后就不能再次使用了;而CyclicBarrier可以重复使用; Semaphore(信号量)

使用Semaphore可以控制同时访问资源的线程个数;

信号量的作用是维护一个许可证的计数,线程可以获取许可证,那信号量剩余的许可证就减一,线程也可以释放一个许可证,那信号量剩余的许可证就加一,当信号量所拥有的许可证数量为0,那么下一个还想要获取许可证的线程,就需要等待,直到有另外的线程释放了许可证;

构造方法:

//可以设置是否要使用公平的策略,如果传入true,那么Semaphore会把之前等待的线程放入到FIFO的队列里,
//以便于有了新的许可证,可以分发给之前等了最长时间的线程
public Semaphore(int permits, boolean fair)

Semaphore的主要方法摘要:

void acquire():从此信号量获取一个许可证,在提供一个许可证前一直将线程阻塞,可以响用中断。boolean tryAcquire():尝试获取许可证,没获取到也不会进行阻塞void release():释放一个许可证,将其返回给信号量。int availablePermits():返回此信号量中当前可用的许可数。boolean hasQueuedThreads():查询是否有线程正在等待获取。

public class SemaphoreDemo {
    static Semaphore semaphore = new Semaphore(2,true);
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            service.submit(new Task());
        }
    }

    static  class Task implements Runnable{
        @Override
        public void run() {
            try {
                semaphore.acquire();
                System.out.println(Thread.currentThread().getName()+"拿到了许可证");
                Thread.sleep(2000);
                semaphore.release();
                //System.out.println(Thread.currentThread().getName()+"释放了许可证");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
Condition

Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效;

Condition是个接口,基本的方法就是await()和signal()方法,并且signal()是公平的;Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition();

调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用

Conditon中的await()对应Object的wait();Condition中的signal()对应Object的notify();Condition中的signalAll()对应Object的notifyAll()

public class ConditionDemo {
    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    void method() throws InterruptedException {
        lock.lock();
        try {
            System.out.println("条件不满足,开始await");
            condition.await();
            System.out.println("条件满足了,开始执行后续的任务");
        }finally {
            lock.unlock();
        }
    }

    void method2(){
        lock.lock();
        try {
            System.out.println("准备工作完成,唤醒其它的线程");
            condition.signal();
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ConditionDemo demo = new ConditionDemo();
        new Thread(() -> {
            try {
                Thread.sleep(1000);
                demo.method2();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        demo.method();
    }
}
//控制台打印:
条件不满足,开始await
准备工作完成,唤醒其它的线程
条件满足了,开始执行后续的任务

那么让执行await()方法的线程暂停运行是什么原理呢?

其实并发包源代码内部执行了Unsafe类中的park方法,让当前线程呈暂停状态:

//isAbsolute代表是否为绝对时间,time表示时间值
//如果isAbsolute传入true,则第二个参数时间单位为毫秒;如果传入false,则第二个参数时间单位为纳秒
public native void park(boolean isAbsolute,long time)

示例如下:程序将会等待3S再输出结果!

public class Test6{
    public static void main(String[] args) throws InterruptedException, NoSuchFieldException, IllegalAccessException {
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe) field.get(null);
        //unsafe.park(false, 3000000000L);   3s对应纳秒为3000000000L
        //unsafe.park(false, 0);   程序将无线等待
        //unsafe.park(true, 0);   程序将无线等待
        unsafe.park(true,System.currentTimeMillis() + 3000);
        System.out.println("-------");
    }
}

Condition 源码最终也是调的这个方法:

    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }

并且Condition可创建多个,用来唤醒指定种类的线程:

class myService{
    Lock lock = new ReentrantLock(true);
    Condition conditionA = lock.newCondition();
    Condition conditionB = lock.newCondition();
    public void methodA(){
        try {
            lock.lock();
            System.out.println("methodA");
            conditionA.await();
            System.out.println("methodA被唤醒");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void methodB(){
        try {
            lock.lock();
            System.out.println("methodB");
            conditionB.await();
            System.out.println("methodB被唤醒");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void signal(){
        try {
            lock.lock();
            conditionA.signal();
        }finally {
            lock.unlock();
        }
    }
}
public class Test6{
    public static void main(String[] args) throws InterruptedException, NoSuchFieldException, IllegalAccessException {
        myService service = new myService();
        new Thread(()->{service.methodA();}).start();
        new Thread(()->{service.methodB();}).start();
        new Thread(()->{service.signal();}).start();
    }
}
//控制台打印
methodA
methodB
methodA被唤醒

awaitUntil(Date deadline)

public boolean awaitUntil(Date deadline):在指定的Date结束等待;

public class Test6{
    static Lock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            try {
                lock.lock();
                condition.awaitUntil(new Date(System.currentTimeMillis()+3000));
                System.out.println("当前时间等待3s自动结束!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }).start();
    }
}

awaitUninterruptibly()

public void awaitUninterruptibly():实现线程在等待的过程中,不允许被中断;

public class Test6{
    static Lock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            try {
                lock.lock();
                condition.awaitUninterruptibly();
                System.out.println("当前时间等待3s自动结束!");
            } finally {
                lock.unlock();
            }
        });
        thread.start();
        Thread.sleep(100);
        thread.interrupt();
        System.out.println(thread.isInterrupted());	//打印false
    }
}
定时器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("------");
    }
}
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/703727.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号