Java多线程详解

Java多线程

并发和并行

并发:多个不同的事务在同一时间间隔执行。

并行:多个不同的事务在同一时间点上执行。

举一个通俗的例子,假设你是一个厨师,因为太忙了,所以你不得不一下子开两个火同时炒两个菜。你的面前是两个锅,如果你技术高超,一只手负责炒一个锅的菜,左右手同时开工,那么这就是并行,因为你同时在执行这两个任务。而如果你的技术不够高超,先炒一边,然后炒几下又迅速跑到另一边再翻炒几下,这样快速的在两个锅前切换,那么这就是并发,因为你在这两个任务之间快速切换。电脑中的cpu就如同这个例子中的厨子,面对一个个任务,cpu在极短的时间内快速的切换任务进行执行,由于切换时间过短,让人感觉像是在同时执行多个任务。

线程和进程

进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.

线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.

线程的创建

继承Thread

Thread类本质上是实现了Runnable接口的一个实例。我们通过继承Thread类的方式,并重写run方法,可以创建自己的线程。调用start()方法可以启动线程。并不推荐使用继承Thread类的方式来创建线程对象:

1、Java是单继承,继承了Thread类后不能再继承其他的类。

2、通过继承的方式数据是线程独享的。

package com.caicai.threaddemo;

/**
 * @Author: caicai
 * @Date: 2020/4/20
 * @Description:
 */
public class MyThreadTest1 {
    public static void main(String[] args) {
        Thread t1 = new Mythread1();
        t1.start();

    }

}
class Mythread1 extends Thread{
    @Override
    public void run() {
        System.out.println("继承Thread创建多线程");
    }
}


实现Runnable接口

相比于继承Thread类,使用Runnable更加灵活,即使是类已经继承了其他的父类,仍然可以通过实现Runnable来支持多线程。只需实现Runnable接口即可。

package com.caicai.threaddemo;

/**
 * @Author: caicai
 * @Date: 2020/4/20
 * @Description:
 */
public class MyThreadTest2 {
    public static void main(String[] args) {
        Thread t2 = new Thread(new Mythread2());
        t2.start();

    }

}

class Mythread2 implements Runnable {
    @Override
    public void run() {
        System.out.println("实现Runnable创建线程");
    }
}


通过构造方法,将Thread类中的target对象赋值,并且在执行的时候调用target的run方法。

public Thread(Runnable target) {
    this(null, target, "Thread-" + nextThreadNum(), 0);
}
@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

实现Callable接口

在jdk1.5之后增加了Callable接口。Callable相对于Runnable有更强大的功能:

  • 相比run方法,可以有返回值
  • 方法可以抛出异常
  • 支持泛型

实现Callable接口的方式不再像前面两种创建Thread对象调用start方法进行执行,而是需要借助FutureTask的run方法来执行。同时调用FutureTask对象的get方法可以获取到线程执行时候的返回值。

package com.caicai.threaddemo;


import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * @Author: caicai
 * @Date: 2020/4/20
 * @Description:
 */
public class MyThreadTest3 {
    public static void main(String[] args) {
        FutureTask<String> futureTask = new FutureTask(new Mythread3());
        futureTask.run();
        String s = null;
        try {
            s = futureTask.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println(s);
    }

}

class Mythread3 implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println("实现Callable创建线程并返回一个值");
        return "hello world";
    }
}

创建线程池

Executors.newFixedThreadPool方法创建线程池。对于经常创建、销毁、使用量特别大的资源使用线程池可以很大的提升性能。线程池的基本思路为提前创建好多个线程,放入线程池中,使用的时候直接从线程池里获取,使用完再放回线程池中。使用的优势在于:

  • 可以避免频繁的创建、销毁线程时对资源的浪费,提升响应速度,降低资源的消耗
  • 便于线程的管理,可以通过设置参数来管理线程池中的线程

《阿里巴巴java开发手册》中提到线程池不使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样 的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

Executors 返回的线程池对象的弊端如下:

  • FixedThreadPool 和 SingleThreadPool : 允许的请求队列长度为 Integer.MAX_VALUE ,可能会堆积大量的请求,从而导致 OOM 。

  • CachedThreadPool 和 ScheduledThreadPool : 允许的创建线程数量为 Integer.MAX_VALUE ,可能会创建大量的线程,从而导致 OOM 。

package com.caicai.threaddemo;
import java.util.concurrent.*;

/**
 * @Author: caicai
 * @Date: 2020/4/20
 * @Description:
 */
public class MyThreadTest4 {
    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3, 7, 10, TimeUnit.SECONDS, new LinkedBlockingDeque<>(10));
        threadPoolExecutor.execute(new Mythread4());
    }

}

class Mythread4 implements Runnable {
    @Override
    public void run() {
        System.out.println("使用线程池创建线程");
    }
}

ThreadPoolExecutor的构造方法如下

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}
名称类型含义
corePoolSizeint核心线程池大小
maximumPoolSizeint最大线程数
keepAliveTimelong线程最大空闲时间
unitTImeUnit时间单位
workQueueBlockingQueue线程等待队列
threadFactoryThreadFactory线程创建工厂
handlerRejectedExecutionHandler拒绝策略

线程常用方法

currentThread()方法

currentThread()方法可以返回代码段正在被哪个线程调用的信息。下面通过一个示例说明。

创建MyThread.java类,代码如下:

package com.caicai.t5;

public class MyThread extends Thread {
    public MyThread() {
        System.out.println("构造方法打印:" + Thread.currentThread().getName());
    }

    @Override
    public void run() {
        System.out.println("run 方法的打印:" + Thread.currentThread().getName());
    }
}

创建Run2.java类,代码如下:

package com.caicai.t5;

public class Run2 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
//        myThread.run();
    }

}

运行结果如下:

构造方法打印:main
run 方法的打印:Thread-0

可以看到MyThread.java的构造函数是被main线程调用的,而run方法是被名称Thread-0的线程调用的,run方法是自动调用的方法。

Run2.java代码更改如下:

package com.caicai.t5;

public class Run2 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
//        myThread.start();
        myThread.run();
    }

}
构造方法打印:main
run 方法的打印:main

构造方法和run方法都是main线程调用的。

isAlive()方法

方法isAlive()的功能是判断当前的线程是否处于活动状态。

创建MyThread.java类,代码如下:

package com.caicai.t7;

public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("run == " + this.isAlive());

    }
}

创建Run.java类,代码如下:

package com.caicai.t7;

public class Run {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        System.out.println("begin==" + myThread.isAlive());
        myThread.start();
        System.out.println("end==" + myThread.isAlive());
    }
}

运行结果如下:

begin==false
end==true
run == true

方法isAlive()的作用是测试线程是否处在活动状态。线程处于正在运行或准备开始运行的状态就认为线程是存活的。需要说明的是

System.out.println("end==" + myThread.isAlive());

虽然示例中打印的是true,但是此值是不确定的。打印true值是因为mythread线程还未执行完毕,所以输出true。

sleep()方法

方法sleep()的作用是在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。这个“正在执行的线程”是指this.currentThread()返回的线程。

通过一个示例来进行说明。

创建MyThread1.java类,代码如下:

package com.caicai.t8;

public class MyThread1 extends Thread {
    @Override
    public void run() {
        try{
            System.out.println("run threadName= " + Thread.currentThread().getName() + "begin ");
            Thread.sleep(2000);
            System.out.println("run threadName= " + Thread.currentThread().getName() + "end ");


        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

创建Run1.java类,代码如下:

package com.caicai.t8;

import org.w3c.dom.ls.LSOutput;

public class Run1 {
    public static void main(String[] args) {
        MyThread1 myThread1 = new MyThread1();
        System.out.println("begin = " + System.currentTimeMillis());
        myThread1.start();
        System.out.println("end = " + System.currentTimeMillis());
    }
}

直接调用run方法运行结果如下:

begin = 1580819435402
run threadName= mainbegin 
run threadName= mainend 
end = 1580819437421

把Run1改为如下:

package com.caicai.t8;


public class Run1 {
    public static void main(String[] args) {
        MyThread1 myThread1 = new MyThread1();
        System.out.println("begin = " + System.currentTimeMillis());
        myThread1.start();
        System.out.println("end = " + System.currentTimeMillis());
    }
}

调用start方法运行结果如下:

begin = 1580819522188
end = 1580819522196
run threadName= Thread-0begin 
run threadName= Thread-0end 

由于main线程与MyThread2线程是异步执行的,所以首先打印的信息为begin和end。而MyThread2线程是随后执行的,在最后两行打印run begin和run end相关信息。

getId()方法

getId()方法的作用是取得线程的唯一标识。

创建Test.java类,代码如下:

package com.caicai.t9;

public class Test {
    public static void main(String[] args) {
        Thread runThread = Thread.currentThread();
        System.out.println(runThread.getName() + "  " + runThread.getId());
    }
}

运行结果如下:

main  1

线程的停止

线程的生命周期

线程的生命周期包括五个阶段,包括:新建、就绪、运行、阻塞、销毁。

  • 新建:使用new 方法new出一个线程,线程就处于新建状态,JVM为线程分配内存,初始化成员变量的值。

  • 就绪:线程调用了start()方法后,线程就处于就绪的状态,JVM为线程创建方法栈和程序计数器。

  • 运行:线程得到了CPU的资源,开始执行,进入运行状态。

  • 阻塞:当发生如下情况时候线程会进入阻塞状态:

    ​ 1)调用sleep()方法,主动放弃所占用的处理器资源

    ​ 2)调用了一个阻塞式IO方法,在该方法返回之前线程被阻塞

    ​ 3)线程试图获得一个同步锁,但是该同步锁正在被其他线程持有。

    ​ 4)等待通知(notify)

    ​ 5)调用了suspend()方法挂起了线程,该方法容易导致死锁,不推荐。

  • 死亡:线程会以如下三种方式结束:

    ​ run()方法或call()方法执行完成,线程正常结束

    ​ 线程抛出一个Exception或者Error

    ​ 调用stop()结束进程,该方法容易导致死锁,不推荐。

线程安全问题

如果有多个线程同时运行了一个实现了Runnable接口的类,程序每次运行结果和单线程运行结果是一样的,那么就是线程安全的,否则就是线程不安全的。

举例分析

举个例子,有几个窗口共同在卖票,并且所有窗口共享所有的票,这样在卖票时候就可能出现线程安全问题。例子代码如下:

package com.caicai.java;

/**
 * @Author: caicai
 * @Date: 2020/4/14
 * @Description:
 */
public class WindowTest1 {
    public static void main(String[] args) {
        Window1 w = new Window1();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);
        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");
        t1.start();
        t2.start();
        t3.start();
    }
}

class Window1 implements Runnable {
    private int ticket = 20;

    @Override
    public void run() {
        while (true) {

            if (ticket > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ":卖票,票号为" + ticket--);
            } else {
                break;
            }
        }

    }
}


代码执行后,运行结果如下:

窗口三:卖票,票号为19
窗口一:卖票,票号为20
窗口二:卖票,票号为20
窗口二:卖票,票号为18
窗口一:卖票,票号为17
窗口三:卖票,票号为17
窗口二:卖票,票号为16
窗口三:卖票,票号为15
窗口一:卖票,票号为14
窗口二:卖票,票号为13
窗口三:卖票,票号为13
窗口一:卖票,票号为12
窗口二:卖票,票号为11
窗口三:卖票,票号为10
窗口一:卖票,票号为9
窗口二:卖票,票号为8
窗口一:卖票,票号为7
窗口三:卖票,票号为7
窗口三:卖票,票号为6
窗口一:卖票,票号为6
窗口二:卖票,票号为6
窗口三:卖票,票号为4
窗口二:卖票,票号为5
窗口一:卖票,票号为5
窗口三:卖票,票号为3
窗口一:卖票,票号为2
窗口二:卖票,票号为2
窗口三:卖票,票号为1
窗口一:卖票,票号为0
窗口二:卖票,票号为-1

从运行结果中,我们看到有的一些窗口卖出的票的序号是相同的,甚至还出现了票号为-1的票。这就是所谓的多个对象同时访问一个成员变量所带来的非线程安全问题。

之所以上述例子中出现了线程安全问题是因为ticket这个变量是三个窗口同时共享的,并且这三个卖票的进程存在对共享数据的写操作。所以若有多个线程同时执行写操作我们一般都要考虑线程同步问题。

实现线程同步

为了解决上述线程问题,只要在某个线程修改共享资源的时候,让其他线程不能修改该资源,等待修改完毕同步后才能去抢夺CPU资源完成响应的操作。Java引入了7中线程同步机制来解决线程同步问题:

  • 同步代码块(synchronized)
  • 同步方法(synchronized)
  • 同步锁(ReentrantLock)
  • 特殊域变量(volatile)
  • 局部变量(ThreadLocal)
  • 阻塞队列(LinkedBlockingQueue)
  • 原子变量(Atomic*)
同步代码块
package com.caicai.java;

/**
 * @Author: caicai
 * @Date: 2020/4/14
 * @Description:
 */
public class WindowTest1 {
    public static void main(String[] args) {
        Window1 w = new Window1();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);
        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");
        t1.start();
        t2.start();
        t3.start();
    }
}

class Window1 implements Runnable {
    private   int ticket = 20;
    @Override
    public void run() {
        while (true) {
//使用synchronized包裹要进行同步的代码,synchronized需要一个对象来当做锁,需要同步的不同线程需要用同一个对象当锁
            synchronized (this) {
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":卖票,票号为" + ticket--);
                } else {
                    break;
                }
            }
        }

    }
}
同步方法
package com.caicai.java;

/**
 * @Author: caicai
 * @Date: 2020/4/14
 * @Description:
 */
public class WindowTest3 {
    public static void main(String[] args) {
        Window3 w = new Window3();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);
        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");
        t1.start();
        t2.start();
        t3.start();
    }
}

class Window3 implements Runnable {
    private int ticket = 20;

    @Override
    public void run() {
        while (true) {
            show();

        }
    }
//使用synchronized来修饰方法,若方法不是静态方法,使用当前对象作为锁(this),若为static方法,使用对象的所属类作为锁(本例中为Window3.class)。
    private synchronized void show() {
        if (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":卖票,票号为" + ticket--);
        }

    }
}

同步锁
ReentrantLock lock = new ReentrantLock(true);//参数,是否公平锁,若true,公平锁,多个线程都公平拥有执行权,false 非公平独占锁,默认值
package com.caicai.java;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author: caicai
 * @Date: 2020/4/14
 * @Description:
 */
public class WindowTest4 {

    public static void main(String[] args) {

        Window4 w = new Window4();
        Window4 w1 = new Window4();
        Window4 w2 = new Window4();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w1);
        Thread t3 = new Thread(w2);
        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");
        t1.start();
        t2.start();
        t3.start();
    }
}

class Window4 implements Runnable {
    private static int ticket = 20;
  	//创建同步锁,此处把锁声明为了static 因为在main方法中,创建线程的时候用了三个不同的Window4对象
    private static ReentrantLock reentrantLock = new ReentrantLock(true);
    @Override
    public void run() {
        while (true) {
          //在同步代码块前边使用lock加锁,为了确保执行了lock之后,unlock一定能执行,把需要同步的代码放在try里,使用finally确保unlock一定执行。
            reentrantLock.lock();
            try {
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":卖票,票号为" + ticket--);
                } else {
                    break;
                }
            } finally {
                reentrantLock.unlock();
            }
        }
    }

}
synchronized和Lock的区别
  • synchronized是java内置关键字,在JVM层面,Lock是个java类
  • synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁
  • synchronized会自动释放锁(a线程执行完同步代码会释放锁,b线程执行过程中发生异常会释放锁),Lock需在finally中手动释放锁(unlock()解锁),否则容易造成线程死锁
  • 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2会一直等待下去。而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了。
  • synchronized的锁可重入、不可中断、非公平,而Lock锁可重入可判断可公平
  • Lock锁适合大量同步代码的同步问题,synchronized锁适合代码少量的同步问题。

线程死锁

线程死锁是指两个或两个以上的线程互相持有对方所需要的资源,由于synchronized的特性,一个线程持有一个资源,或者说获得一个锁,在该线程释放这个锁之前,其它线程是获取不到这个锁的,而且会一直死等下去,因此这便造成了死锁。举个栗子,两个人一起吃饭,但餐桌上只有一双筷子,秉承着谁先抢到筷子谁先吃的原则两个人开始抢筷子,碰巧一人抢到了一根筷子,对于一般人来说一根筷子显然是没法夹东西的,而两个人对于自己手里抢到的一根筷子都不愿意松手,所以就陷入了无限的等待当中。

产生死锁的必要条件

  • 互斥条件:

    进程要求对锁分配的资源进行排他性控制,即在一段时间内某资源仅为一个进程占有。此时若有其他进程请求该资源,则请求进程只能等待。

  • 不可剥夺条件:

    进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)

  • 请求与保持条件

    进程已经保持了至少一个资源,但又提出了新的资源请求,该资源已经被其他进程占有,此时请求阻塞,但对自己获得的资源保持不放

  • 循环等待条件

    存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中的下一个进程所请求。

死锁演示

package com.caicai.threadlock;

/**
 * @Author: caicai
 * @Date: 2020/4/21
 * @Description:
 */

public class Demo {

    public static void main(String[] args) {
        Person1 p1 = new Person1();
        Person2 p2 = new Person2();
        Thread t1 = new Thread(p1);
        Thread t2 = new Thread(p2);
        t1.setName("A");
        t2.setName("B");
        t1.start();
        t2.start();
    }


}

class Person1 implements Runnable {
    @Override
    public void run() {
        synchronized (Chopstick1.class) {
            System.out.println(Thread.currentThread().getName() + "拿到了1号筷子去拿2号筷子");
            synchronized (Chopstick2.class) {
                System.out.println(Thread.currentThread().getName() + "拿到了所有筷子,吃饭了");
            }
        }

    }
}

class Person2 implements Runnable {
    @Override
    public void run() {
        synchronized (Chopstick2.class) {
            System.out.println(Thread.currentThread().getName() + "拿到了2号筷子去拿1号筷子");
            synchronized (Chopstick1.class) {
                System.out.println(Thread.currentThread().getName() + "拿到了所有筷子,吃饭了");
            }
        }

    }
}

//一号筷子
class Chopstick1 {

}
//二号筷子
class Chopstick2 {

}

在上述代码中,分别有Person1,Person2两个类代表两个人,分别叫A和B,Chopstick1、Chopstick2两个类代表两根筷子,分别为1号筷子和2号筷子。当一个人拿到两根筷子后进行吃饭。对于A来说,他先拿取1号筷子,然后去拿2号筷子,B刚好与A相反,他先拿2号筷子再拿1号筷子。运行结果如下:

B拿到了2号筷子去拿1号筷子
A拿到了1号筷子去拿2号筷子

A和B分别抢到了一根筷子等待另一根筷子,然而双方都不愿意放弃自己手中的筷子,造成死锁,程序无限的等待下去。

死锁处理

  • 预防死锁:

    通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或几个条件,来防止死锁的发生。

  • 避免死锁

    在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免死锁的发生。

  • 检测死锁

    允许系统在运行过程中发生死锁,但可设置检测机构及时检测死锁的发生,并采取适当措施加以清除。

  • 解除死锁

    当检测出死锁后,便采取适当措施将进程从死锁状态中解脱出来。

线程通讯

对个线程并发执行时,在默认情况下CUP是随机切换线程的,有时我们希望CPU按我们的规律执行线程,此时就需要线程之间的协调通信。

线程通信方式

线程间通信常用方式如下:

  • 休眠唤醒的方式:

    Object的wait、notify、notifyAll

    Condition的await、signal、signalAll

  • CountDownLatch:用于某个线程A等待若干个其他线程执行完后,他才执行

  • CyclicBarrier:一组线程等待至某个状态之后再同时执行

  • Semaphore:用于控制对某组资源的访问权限

wait/notify使用举例
  • Object.wait():释放当前对象锁,并进入阻塞队列
  • Object.notify():唤醒当前对象阻塞队列里的任一线程(并不保证唤醒哪一个)
  • Object.notifyAll():唤醒当前对象阻塞队列里的所有线程

使用wait和notify关键字让两个线程交替打印奇数和偶数。代码如下:

package com.caicai;

/**
 * @Author: caicai
 * @Date: 2020/4/22
 * @Description:
 */
public class ThreadCommunication {

    public static void main(String[] args) {
        OddEvenDemo demo = new OddEvenDemo();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                demo.odd();

            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                demo.even();

            }
        });
        t1.start();
        t2.start();
    }
}


class OddEvenDemo {
    private int i = 0;

    public synchronized void odd() {
        while (i < 10) {
            if (i % 2 == 1) {
                System.out.println("奇数 " + i);
                i++;
              //唤醒偶数线程
                notify();
            } else {
                try {
                  //当前为偶数,进入阻塞,
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public synchronized void even() {
        while (i < 10) {
            if (i % 2 == 0) {
                System.out.println("偶数 " + i);
                i++;
              //唤醒奇数线程
                notify();
            } else {
                try {
                 //当前为奇数,进入阻塞
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

注意:wait/notify 要与synchronized一起使用,不然会出现java.lang.IllegalMonitorStateException异常

await/signal使用举例

await/signal是Condition接口里的方法,配合lock一起使用也可以完成线程通讯的功能。仍是两个线程交替打印奇数和偶数,代码如下:

package com.caicai;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author: caicai
 * @Date: 2020/4/22
 * @Description:
 */
public class ThreadCommunication2 {

    public static void main(String[] args) {
        OddEvenDemo demo = new OddEvenDemo();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                demo.odd();

            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                demo.even();

            }
        });
        t1.start();
        t2.start();
    }
}


class OddEvenDemo2 {
    private int i = 0;
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void odd() {
        while (i < 10) {
            lock.lock();
            try {
                if (i % 2 == 1) {
                    System.out.println("奇数 " + i);
                    i++;
                    condition.signal();
                } else {
                    try {
                        condition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } finally {
                lock.unlock();

            }
        }
    }

    public void even() {
        while (i < 10) {
            lock.lock();
            try {
                if (i % 2 == 0) {
                    System.out.println("偶数 " + i);
                    i++;
                    condition.signal();
                } else {
                    try {
                        condition.await();

                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } finally {
                lock.unlock();

            }
        }
    }

}
Object和Condition休眠唤醒的区别
  • Object wait()必须在synchronized(同步锁)下使用
  • Object wait()必须通过notify()方法进行唤醒
  • Condition await()必须和Lock(互斥锁/共享锁)配合使用
  • Condition await()必须通过signal()方法进行唤醒
CountDownLatch方式
  • CountDownLatch是在java1.5被引入,存在于java.util.cucurrent包下,跟它一起被引入的工具类还有CyclicBarrier。

  • CountDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。

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

    Java的concurrent包里面的CountDownLatch其实可以把它看作一个计数器,只不过这个计数器的操作是原子操作,同时只能有一个线程去操作这个计数器,也就是同时只能有一个线程去减这个计数器里面的值。

    你可以向CountDownLatch对象设置一个初始的数字作为计数值,任何调用这个对象上的await()方法都会阻塞,直到这个计数器的计数值被其他的线程减为0为止。

代码示例如下:

package com.caicai;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @Author: caicai
 * @Date: 2020/4/22
 * @Description: 学生由于疫情原因只能在家进行上网课, 老师需要等到全部学生进入课堂后才开始上课, 假设有20个学生.
 */
public class CountDownLatchDemo {

    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        CountDownLatch countDownLatch = new CountDownLatch(20);
        service.execute(new Teacher(countDownLatch));

        for (int i = 0; i < 20; i++) {
            service.execute(new Student("学生" + (i + 1) + "号", countDownLatch));

        }
        service.shutdown();

    }

}

class Student implements Runnable {
    public Student(String name, CountDownLatch countDownLatch) {
        this.name = name;
        this.countDownLatch = countDownLatch;
    }

    private String name;
    private CountDownLatch countDownLatch;

    @Override
    public void run() {
      
        System.out.println(name + "准备就绪!!");
        countDownLatch.countDown();


    }
}

class Teacher implements Runnable {
    public Teacher(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }

    private CountDownLatch countDownLatch;

    @Override
    public void run() {
        System.out.println("等待学生进入课堂");
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("人到齐了,开始上课!");

    }
}

运行结果:

等待学生进入课堂
学生6号准备就绪!!
学生1号准备就绪!!
学生8号准备就绪!!
学生7号准备就绪!!
学生5号准备就绪!!
学生4号准备就绪!!
学生3号准备就绪!!
学生2号准备就绪!!
学生10号准备就绪!!
学生9号准备就绪!!
学生11号准备就绪!!
学生13号准备就绪!!
学生16号准备就绪!!
学生18号准备就绪!!
学生12号准备就绪!!
学生20号准备就绪!!
学生19号准备就绪!!
学生17号准备就绪!!
学生15号准备就绪!!
学生14号准备就绪!!
人到齐了,开始上课!

上述例子中每创建一个学生线程都使用countDown()方法来减少一个计数,countDown方法的注释大意如下:

减少锁存器的计数,如果计数达到零,则释放所有等待线程。如果当前计数大于零,则将其递减。如果新计数为零,则出于线程调度的目的,将重新启用所有等待线程。如果当前计数等于零,那么什么也不会发生。
CyclicBarrier

CyclicBarrier也是在jdk1.5后被引入的。CyclicBarrier让一组线程都处于某个状态后在全部同时执行。CyclicBarrier的底层是基于ReentrantLock和Condition的实现。代码示例如下:

package com.caicai;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @Author: caicai
 * @Date: 2020/4/22
 * @Description:
 * 三个爬山爱好者,约定在山脚下,半山腰,山顶分别集合一次再进行下一步活动.
 */
public class CyclicBarrierDemo {

    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();

        CyclicBarrier cyclicBarrier = new CyclicBarrier(3,()->{
            System.out.println(Thread.currentThread().getName() +"最后到达,大家都到了我来晚了抱歉,继续进行下一步吧");
        });
        for (int i = 1; i < 4; i ++){
            service.execute(new Person(cyclicBarrier,i+"号"));
        }
        service.shutdown();
    }


}

class Person implements Runnable {
    public Person(CyclicBarrier cyclicBarrier, String name) {
        this.cyclicBarrier = cyclicBarrier;
        this.name = name;
    }

    public String getName() {
        return name;
    }

    private CyclicBarrier cyclicBarrier;
    private String name;


    @Override
    public void run() {
        try {
            System.out.println(getName() + "到达山脚下");
            cyclicBarrier.await();
            System.out.println(getName() + "从山脚下出发");
            Thread.sleep(1000);
            System.out.println(getName() + "到达半山腰");
            cyclicBarrier.await();
            System.out.println(getName() + "从半山腰出发");
            Thread.sleep(1000);

            System.out.println(getName() + "到达山顶");
            cyclicBarrier.await();
            System.out.println(getName() + "与大家庆祝拍照留念");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }


    }
}


代码运行结果如下:

2号到达山脚下
3号到达山脚下
1号到达山脚下
pool-1-thread-3最后到达,大家都到了我来晚了抱歉,继续进行下一步吧
3号从山脚下出发
2号从山脚下出发
1号从山脚下出发
1号到达半山腰
3号到达半山腰
2号到达半山腰
pool-1-thread-2最后到达,大家都到了我来晚了抱歉,继续进行下一步吧
2号从半山腰出发
1号从半山腰出发
3号从半山腰出发
1号到达山顶
2号到达山顶
3号到达山顶
pool-1-thread-3最后到达,大家都到了我来晚了抱歉,继续进行下一步吧
3号与大家庆祝拍照留念
1号与大家庆祝拍照留念
2号与大家庆祝拍照留念

从打印结果可以看出,所有线程会等待全部线程到达之后才会继续执行,并且最后到达的线程会完成 Runnable 的任务。

CyclicBarrier 与 CountDownLatch 区别
CountDownLatchCyclicBarrier
减计数方式加计数方式
计算为0时释放所有等待的线程计数达到指定值时释放所有等待线程
计数为0时,无法重置计数达到指定值时,计数置为0重新开始
调用countDown()方法计数减一,调用await()方法只进行阻塞,对计数没任何影响调用await()方法计数加1,若加1后的值不等于构造方法的值,则线程阻塞
不可重复利用可重复利用
线程在countDown()之后,会继续执行自己的任务所有线程任务结束之后,才会进行后续任务
Semaphore方式

Semaphore是java1.5被引入,存在于java.util.concurrent包下

Semaphore用于控制线程的并发数量。假设疫情期间有一个商店为了防止人数过多,要求大家排队,每次只允许两个消费者进店消费。代码如下:

package com.caicai;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

/**
 * @Author: caicai
 * @Date: 2020/4/23
 * @Description:
 */
public class SemaphoreDemo {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(2);

        ExecutorService e = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            e.execute(new Consumer(semaphore, i));
        }
        e.shutdown();


    }

}

class Consumer implements Runnable {

    private Semaphore semaphore;
    private Integer id;

    public Consumer(Semaphore semaphore, Integer id) {
        this.semaphore = semaphore;
        this.id = id;
    }

    @Override
    public void run() {
        try {
            semaphore.acquire();
            System.out.println(id + "号消费者进店购买");
            Thread.sleep(2000);

            System.out.println(id + "号消费者离开");
            semaphore.release();

        } catch (InterruptedException e) {
            e.printStackTrace();
        }


    }
}

代码运行结果如下:

0号消费者进店购买
1号消费者进店购买
1号消费者离开
0号消费者离开
4号消费者进店购买
2号消费者进店购买
2号消费者离开
4号消费者离开
5号消费者进店购买
6号消费者进店购买
5号消费者离开
6号消费者离开
3号消费者进店购买
7号消费者进店购买
3号消费者离开
7号消费者离开
8号消费者进店购买
9号消费者进店购买
9号消费者离开
8号消费者离开

通过使用Semaphore的acquire()方法和release()方法实现控制并发线程的数量。此外使用acquire(int permits)方法可以让一个线程占据自定义个许可。对应的release(int permits) 来释放指定许可。

多线程特性

多线程编程要保证满足三个特性:原子性、可见性、有序性。

原子性

原子性是指一个操作或者多个操作要么全部执行并且执行的过程中不会被任何因素打断,要么就不执行。

可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值。对于多线程,不存在可见性问题。

有序性

有序性即程序执行的顺序按照代码的先后顺序执行。

多线程控制类

为了保证多线程的三个特性,Java引入了很多线程控制机制:

  • ThreadLocal:线程本地变量
  • 原子类:保证变量原子操作
  • Lock类:保证线程有序性
  • Volatile关键字:保证线程变量可见性

ThreadLocal

作用

官方解释如下:

/**
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code ThreadLocal} instances are typically private
 * static fields in classes that wish to associate state with a thread (e.g.,
 * a user ID or Transaction ID).
 */

大致意思是说该类为每个线程都提供了一个自己独立的变量,通过get和set方法就可以对本线程的变量进行操作。

应用场景

在Java的多线程编程中,为保证多个线程对共享变量的安全访问,通常会使用synchronized来保证同一时刻只有一个线程对共享变量进行操作。这种情况下可以将类变量放到ThreadLocal类型的对象中,使变量在每个线程中都有独立拷贝,不会出现一个线程读取变量时而被另一个线程修改的现象。最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理等。

举个栗子

两个人都在同一个银行里进行存款,银行是相同的,但是用户的账户余额应该是独立的,用ThreadLocal完成此功能。

package com.caicai;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @Author: caicai
 * @Date: 2020/4/30
 * @Description:
 */
public class ThreadLocalDemo {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        Bank b = new Bank();
        executorService.execute(new Transfer(b));
        executorService.execute(new Transfer(b));
        executorService.shutdown();
    }

}
class Bank {

    private ThreadLocal<Integer> threadLocal = new ThreadLocal<>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public Integer get() {

        return threadLocal.get();
    }

    public void set(Integer value) {
        threadLocal.set(value + threadLocal.get());
    }


}
class Transfer implements Runnable{
    private  Bank bank ;

    Transfer(Bank bank) {
        this.bank = bank;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++){
            Integer integer = bank.get();
            int v = (int) (Math.random() * 10);
            bank.set(v);
            System.out.println("原来余额" +integer+",  "+Thread.currentThread().getName() +"向银行存了" + v +"元,余额为" +bank.get() +"元");

        }


    }
}

运行结果如下

原来余额0,  pool-1-thread-1向银行存了0,余额为0元
原来余额0,  pool-1-thread-2向银行存了4,余额为4元
原来余额0,  pool-1-thread-1向银行存了0,余额为0元
原来余额4,  pool-1-thread-2向银行存了2,余额为6元
原来余额0,  pool-1-thread-1向银行存了9,余额为9元
原来余额6,  pool-1-thread-2向银行存了9,余额为15元
原来余额9,  pool-1-thread-1向银行存了8,余额为17元
原来余额15,  pool-1-thread-2向银行存了2,余额为17元
原来余额17,  pool-1-thread-1向银行存了3,余额为20元
原来余额17,  pool-1-thread-2向银行存了3,余额为20元
原来余额20,  pool-1-thread-1向银行存了2,余额为22元
原来余额20,  pool-1-thread-2向银行存了7,余额为27元
原来余额22,  pool-1-thread-1向银行存了9,余额为31元
原来余额27,  pool-1-thread-2向银行存了0,余额为27元
原来余额31,  pool-1-thread-1向银行存了0,余额为31元
原来余额27,  pool-1-thread-2向银行存了7,余额为34元
原来余额31,  pool-1-thread-1向银行存了6,余额为37元
原来余额34,  pool-1-thread-2向银行存了1,余额为35元
原来余额37,  pool-1-thread-1向银行存了6,余额为43元
原来余额35,  pool-1-thread-2向银行存了7,余额为42

可以看线程1和线程2都在向银行里进行存款,两个用户的存款余额是独立的

原子类

Java的java.util.concurrent.atomic包里提供了很多可以进行原子操作的类,分为以下四类:

  • 原子更新基本类型:AtomicInteger、AtomicBoolean、AtomicLong
  • 原子更新数组类型:AtomicIntegerArray、AtomicLongArray
  • 原子更新引用类型:AtomicReference、AtomicStampedReference
  • 原子更新属性类型:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater

提供这些原子类的目的是为了解决基本类型操作的非原子性导致在多线程并发访问情况下引发的问题。

非原子性操作问题演示

使用两个线程同时对n进行++操作,观察n的最终值。

n++操作并不是原子操作,虽然他只有一行,它由三个操作组成:

tp1 = n;
tp2 = tp1 + 1;
n = tp2;

所以如果只是单线程下,n的值不会有问题,但是在多线程情况下就有可能出错。示例代码如下:

package com.caicai;

/**
 * @Author: caicai
 * @Date: 2020/4/30
 * @Description:
 */
public class ThreadAtomicDemo {
    private static int n = 0;

    public static void main(String[] args) {
        int  j = 0;
        while(j<20){
            n= 0;
            Thread thread = new Thread(() -> {
                for (int i = 0; i < 1000; i++) {
                    n++;

                }
            });
            Thread thread1 = new Thread(() -> {
                for (int i = 0; i < 1000; i++) {
                    n++;
                }
            });
            thread.start();
            thread1.start();
            try {
                thread.join();
                thread1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(n);
            j++;
        }


    }
}

运行结果如下:

2000
1627
2000
2000
2000
2000
2000
2000
2000
2000
2000
2000
2000
2000
2000
2000
2000
2000
2000
2000

两个线程每个执行了1000次n++操作,理论上讲n的最终值应该是2000,但是从结果中可以看到出现了为1627的值。这也证明了++操作在 多线程情况下是不安全的

原子类解决非原子性操作问题

AtomicInteger类可以保证++操作的原子性:

  • getAndIncrement():对应n++
  • incrementAndGet():对应++n
  • decrementAndGet():对应–n
  • getAndDecrement():对应n–

修改代码如下:

package com.caicai;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @Author: caicai
 * @Date: 2020/4/30
 * @Description:
 */
public class ThreadAtomicDemo {
//    private static int n = 0;
    private static AtomicInteger n ;
    public static void main(String[] args) {
        int  j = 0;
        while(j<20){
            n= new AtomicInteger(0);//创建原子整数,初始值为0
            Thread thread = new Thread(() -> {
                for (int i = 0; i < 1000; i++) {
                    n.getAndIncrement();

                }
            });
            Thread thread1 = new Thread(() -> {
                for (int i = 0; i < 1000; i++) {
                    n.getAndIncrement();
                }
            });
            thread.start();
            thread1.start();
            try {
                thread.join();
                thread1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(n);
            j++;
        }


    }
}

CAS算法和ABA问题

乐观锁和悲观锁的概念
  • 悲观锁:悲观锁认为自己执行的操作过程中一定会有人修改自己操作的值,所以在自己操作之前一定会加上一把锁。Synchronized就是一个悲观锁。
  • 乐观锁:乐观锁每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS,Compare and Swap。
CAS算法

CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。

CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。

在AtomicInteger对象实现n++操作的函数getAndIncrement中就使用了CAS

public final int getAndIncrement() {
    return U.getAndAddInt(this, VALUE, 1);
}

上述代码的U是AtomicInteger中一个Unsafe的对象。

private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();

Unsafe是CAS的核心类,Java无法直接访问底层操作系统,而是通过本地native方法来访问,尽管如此,JVM还是开了一个后门:Unsafe它提供了硬件级别的原子操作。

    /**
     * Atomically adds the given value to the current value of a field
     * or array element within the given object {@code o}
     * at the given {@code offset}.
     *
     * @param o object/array to update the field/element in
     * @param offset field/element offset
     * @param delta the value to add
     * @return the previous value
     * @since 1.8
     */
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!weakCompareAndSetInt(o, offset, v, v + delta));
    return v;
}

getIntVolatile方法拿到内存位置的最新值v,使用CAS尝试将内存位置的值v修改为目标值v+delta,如果修改失败,则获取该内存位置的新值v,然后继续尝试,直至修改成功。

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前 值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”

通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新 值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。

类似于 CAS 的指令允许算法执行读-修改-写操作,而无需害怕其他线程同时 修改变量,因为如果其他线程修改变量,那么 CAS 会检测它(并失败),算法 可以对该操作重新计算。

CAS缺点

CAS搞笑的解决了原子操作问题,但是CAS仍然存在问题

  • 循环时间长开销很大。
  • 只能保证一个共享变量的原子操作。
  • ABA问题
ABA问题

因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。

从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

使用AtomicStampedReference解决ABA问题

AtomicStampedReference主要方法:

  • AtomicStampedReference(初始值,时间戳):构造函数,设置初始值和初始时间戳
  • getStamp:获取时间戳
  • getReference:获取预期值
  • compareAndSet(预期值,更新至,预期时间戳,更新时间戳)实现CAS时间戳和预期值的比对

compareAndSet和weakCompareAndSet的差别:

weakCompareAndSet无法保证处理操作目标的volatile变量外的其他变量的执行顺序

package com.caicai;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * @Author: caicai
 * @Date: 2020/4/30
 * @Description:
 */
public class ThreadAtomicDemo {
    //    private static int n = 0;
//    private static AtomicInteger n ;
    private static AtomicStampedReference<Integer> atomicInteger;

    public static void main(String[] args) {
        int j = 0;
        while (j < 20) {
            atomicInteger = new AtomicStampedReference(0, 0);//创建原子整数,初始值为0
            Thread thread = new Thread(() -> {
                for (int i = 0; i < 1000; i++) {
                    int stamp;
                    Integer reference;
                    do {
                        reference = atomicInteger.getReference();
                        stamp = atomicInteger.getStamp();


                    } while (!atomicInteger.weakCompareAndSet(reference, reference + 1, stamp, stamp + 1));

                }
            });
            Thread thread1 = new Thread(() -> {
                for (int i = 0; i < 1000; i++) {
                    int stamp;
                    Integer reference;
                    do {
                        reference = atomicInteger.getReference();
                        stamp = atomicInteger.getStamp();


                    } while (!atomicInteger.weakCompareAndSet(reference, reference + 1, stamp, stamp + 1));

                }
            });
            thread.start();
            thread1.start();
            try {
                thread.join();
                thread1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicInteger.getReference());
            j++;
        }


    }
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值