JUC
1.什么是JUC
juc就是 java.util.concurrent 的简称,指的是java为多线程并发提供的工具类。工具类再以下三个包内
2. 进程和线程
2.1 进程和线程是什么?
进程:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
线程:通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义,线程可以利用进程所有拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。
2.2 并发和并行是什么?
并发和并行是两个非常容易混淆的概念。它们都可以表示两个或多个任务一起执行,但是偏重点有点不同。并发偏重于多个任务交替执行,而多个任务之间各自串行的。并发是逻辑上的同时发生(simultaneous),而并行是物理上的同时发生。然而并行的偏重点在于”同时执行”。
严格意义上来说,并行的多个任务是真实的同时执行,而对于并发来说,这个过程只是交替的,一会运行任务一,一会儿又运行任务二,系统会不停地在两者间切换。但对于外部观察者来说,即使多个任务是串行并发的,也会造成是多个任务并行执行的错觉。
实际上,如果系统内只有一个CPU,而现在而使用多线程或者多线程任务,那么真实环境中这些任务不可能真实并行的,毕竟一个CPU一次只能执行一条指令,这种情况下多线程或者多线程任务就是并发的,而不是并行,操作系统会不停的切换任务。真正的并发也只能够出现在拥有多个CPU的系统中(多核CPU)。
2.3 线程的6中状态
计算机的线程有6中状态,如下:
public enum State {
//线程刚创建
NEW,
//在JVM中正在运行的线程
RUNNABLE,
//线程处于阻塞状态,等待监视锁,可以重新进行同步代码块中执行
BLOCKED,
//等待状态
WAITING,
//超时等待状态
TIMED_WAITING,
// 线程执行完毕,已经退出
TERMINATED;
}
具体流程图如下:
2.4 wait和sleep方法的区别
1、来自不同的类
这两个方法来自不同的类分别是,sleep来自Thread类,wait来自Object类。sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用了b的sleep方法,实际上还是a去睡觉,要让b线程睡觉要在b的代码中调用sleep。
2、有没有释放锁(释放资源)
最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。sleep是线程被调用时,占着cpu去睡觉,其他线程不能占用cpu,os认为该线程正在工作,不会让出系统资源,wait是进入等待池等待,让出系统资源,其他线程可以占用cpu。
sleep(100L)是占用cpu,线程休眠100毫秒,其他进程不能再占用cpu资源,wait(100L)是进入等待池中等待,交出cpu等系统资源供其他进程使用,在这100毫秒中,该线程可以被其他线程notify,但不同的是其他在等待池中的线程不被notify不会出来,但这个线程在等待100毫秒后会自动进入就绪队列等待系统分配资源,换句话说,sleep(100)在100毫秒后肯定会运行,但wait在100毫秒后还有等待os调用分配资源,所以wait100的停止运行时间是不确定的,但至少是100毫秒。
3、使用范围不同
wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用(因为它是Thread类的静态方法)
public synchronized void decrement() throws InterruptedException {
while (num != 1) {
// wait、notify、notifyAll 方法只能够在 synchronized 修饰的地方进行调用。
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName() + "进行了-1操作,当前num:" + num);
this.notifyAll();
}
关于为什么 wait()、notify()等方法必须在 synchronized 修饰的方法或者代码块中运行?
wait()
、notify()
和 notifyAll()
是 Java 中用于线程间通信的方法,它们必须在 synchronized
方法或代码块中调用。这是因为这些方法依赖于对象的监视器锁(monitor lock),而 synchronized
是获取和释放监视器锁的唯一方式。
举例: wait()方法就表示当前调用该方法的线程要进行 WAITING 状态,所以要释放自己所拿到的锁(通过synchronized的监视器锁)和线程所持有的资源,那么这个线程要有锁才可以释放,而一个线程要有锁就必须在 synchronized修饰的地方。
现在这里说的锁都是关于 synchronized 的锁。不包括后边要说的 juc内提供的锁(eg: ReentrantLock 之类的锁。)
4、是否需要捕获异常
sleep必须捕获异常,而wait,notify和notifyAll不需要捕获获异常
重点总结:
sleep()方法不需要释放当前线程所拥有的锁和共享资源。而wait()方法需要 释放当前线程所持有的锁和资源 然后线程再进入 WAITING 状态(可以理解为线程被阻塞),同时如果线程被 notify()、notifyAll() 方法唤醒后需要 重新获取锁和共享资源,如果唤醒后仍然没有抢到锁,那么就会继续阻塞。
3 lock锁
3.1 synchronized
先看一下传统的使用 synchronized 来实现线程安全的方式
① 题目:多线程三个售票员 卖出 30张票
package com.llh.juc.package1;
public class SaleTicketTest1 {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() -> {
for (int x = 1; x<=2000;x++){
ticket.sale();
}
},"线程A").start();
new Thread(() -> {
for (int x = 1; x<=2000;x++){
ticket.sale();
}
},"线程B").start();
new Thread(() -> {
for (int x = 1; x<=2000;x++){
ticket.sale();
}
},"线程C").start();
}
}
class Ticket {
private int num = 5000;
// 使用 synchronized 来对该方法进行上锁,来实现多线程的线程安全。让同一时刻只能有一个线程来调用该方法。
public synchronized void sale() {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + " 卖出了一张票,还剩余:" + num-- + "张");
}
}
}
上边是使用传统的 synchronized 来保证多线程高并发的线程安全。接下来介绍一下juc内提供的Lock。
3.2 Lock
lock锁演示
public class SaleTicketTest2 {
public static void main(String[] args) throws Exception { // main一切程序的入口
Ticket ticket = new Ticket();
new Thread(()->{for (int i = 1; i <=40; i++) ticket.saleTicket();},
"A").start();
new Thread(()->{for (int i = 1; i <=40; i++) ticket.saleTicket();},
"B").start();
new Thread(()->{for (int i = 1; i <=40; i++) ticket.saleTicket();},
"C").start();
}
}
class Ticket{ // 资源类
// 创建一个用来管理lock锁的对象
private Lock lock = new ReentrantLock();
private int number = 30;
public void saleTicket(){
// .lock() 获取锁,如果没有获取到锁那么线程就阻塞在这里,如果获取到锁就继续向下执行
lock.lock();
try {
if (number>0){
System.out.println(Thread.currentThread().getName() + "卖出第" + (number--) + "票,还剩下:"+number);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// .unlock() 方法当前线程所获取到的锁。 unlock() 一定要放在finally内执行,防止出现异常导致锁没有被释放,那么可能会导致死锁。
lock.unlock();
}
}
}
4 生产者和消费者模型
生产者和消费者模型主要就是要考虑的是 线程间的通信 , 线程之间要协调和调度
4.1 synchronized方式
package com.llh.juc.package1;
public class MyTest02 {
public static void main(String[] args) {
/** 为了防止线程的虚假唤醒,所以这个地方采用 while 来进行条件的判断
* 如果当前操作因为条件不符合而执行了wait(),但是如果其它线程由于执行了 notifyAll()方法就会把该方法唤醒,那么如果是使用if来进行条件判断的话,当* 线程被唤醒后就会继续延续刚才的代码继续执行,而不会重新走条件判断,所以这里就存在了隐患,当该线程被唤醒,
* 但是又因为此时该线程的条件仍然不符合,同时因为是if判断,所以就没有进行再次验证,那么这样就会造成错误的代码执行。(可以理解为虚假唤醒)
* 如果使用的while进行条件判断的话,那么当线程被唤醒后,该线程不是直接继续执行没执行的代码,而是看 while(condition){this.wait()} 这里的while里面
* 条件有没有满足,就是再次做了验证,如果仍然不满足的话,那么久继续等待。所以这里就实现了防止 虚假唤醒。
*/
MyData myData = new MyData();
new Thread(() -> {
for (int i = 0;i<10;i++){
try {
myData.increment();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"生产者").start();
new Thread(()->{
for (int i = 0;i<10;i++){
try {
myData.decrement();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"消费者").start();
}
}
class MyData{
// 定义一个共享资源
private int num = 0;
public synchronized void increment() throws InterruptedException {
// 如果num不为0那就先阻塞,等num为0了在进行 +1 操作
while (num != 0) {
// num 不等于 0,调用 wait() 让当前线程阻塞,并释放锁
this.wait();
}
num++;
System.out.println(Thread.currentThread().getName() + "进行了+1,当前num=:" + num);
// 生产好了,通知唤醒所有被阻塞的线程。这里主要是通知消费者,让消费者苏醒并重新获取锁进行消费。
this.notifyAll();
}
// 消费者和生产者的原理一样
public synchronized void decrement() throws InterruptedException {
while (num != 1){
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName() + "进行了-1,当前num=:" +num);
this.notifyAll();
}
}
注意上边在进行线程阻塞条件判断的时候需要使用 while() 而不是 if()。通过使用 while() 来进行判断就是为了防止线程的虚假唤醒的问题的。
4.2 Lock方式
使用Lock的时候要先介绍以下 Condition,通过使用 Condition 来配合 Lock。
Condition的作用等价于 Synchronized 中的监视器锁,并且condition是一个队列,用来存储那些线程。不同动作的线程可以被存放在多个不同的condition,condition通过 await()、singal()方法给存储在本condition中的线程进行通信,从而就实现了 线程间通信 的功能。
如上边的实例代码中, notFull 这个condition用在了 数组不能为满的条件中,并且通过 notFull.await() 方法将当前线程线程加入 notFull的队列中,同样notEmpty 也是如此。 所以notFull中就存储了 条件为 数组不能为满 的线程,而notEmpty中就存储了条件为 数组不能为空的线程,通过这样将不同需求的线程加入不同的condition中。
① 使用Lock的方式实现 生产者- 消费者
package com.llh.juc.package1;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyTest05 {
public static void main(String[] args) {
MyData05 myData05 = new MyData05();
new Thread(()->{
for (int i = 0;i<10;i++){
try {
myData05.increment();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"生产者").start();
new Thread(()->{
for (int i = 0;i<10;i++){
try {
myData05.decrement();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"消费者").start();
new Thread(()->{
for (int i = 0;i<10;i++){
try {
myData05.increment();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"生产者2").start();
new Thread(()->{
for (int i = 0;i<10;i++){
try {
myData05.decrement();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"消费者2").start();
}
}
class MyData05{
private int num = 0;
private Lock lock = new ReentrantLock(); // 创建一个可重入锁对象
private Condition produceCondition = lock.newCondition(); // 生产者线程监视队列
private Condition consumeCondition = lock.newCondition(); // 消费者线程监视队列
public void increment() throws InterruptedException {
lock.lock(); // 获取锁,如果没拿到锁就在这里阻塞。如果拿到的话就向下执行
try {
// 判断该不该这个线程来执行
while (num != 0) {
// 如果不满足条件,那么就先原子形式释放锁,并且当前线程进入阻塞 WAITING 状态。
// 直到被唤醒并且再次获得锁,并且通过该条件判断。如果不满足 唤醒、得到锁、通过条件判断 中的任意一个,那么就继续进入阻塞
// 同时因为该线程调用了 produceCondition ,那么该线程就被加入了 produceCondition 监视队列中,该线程就会收到 produceCondition 的通知,eg:await(),signal()
produceCondition.await();
}
// 执行业务代码
num ++;
System.out.println(Thread.currentThread().getName()+"进行了+1操作,当前num = " + num);
// 唤醒 consumeCondition 监视队列中的线程,让它们准备抢锁并执行代码
consumeCondition.signalAll();
}catch (Exception e){
System.out.println(e.getMessage());
} finally {
// 释放该线程的锁
lock.unlock();
}
}
public void decrement() throws InterruptedException {
// 获取锁
lock.lock();
try {
while (num != 1){
consumeCondition.await();
}
// 执行业务代码
num--;
System.out.println(Thread.currentThread().getName() + "进行了 -1 操作,当前num = " +num);
produceCondition.signalAll();
}catch (Exception e){
System.out.println(e.getMessage());
}finally {
lock.unlock();
}
}
}
思考: 这个地方 生产者 和 消费者 必须使用一个锁对象吗? 不能生产者一把锁 消费者一把锁吗?
② 进阶练习: 使用Lock锁的方式,精确控制线程之间的顺序执行
思路就是使用 标志位和condition的线程通知特性。
package com.llh.juc.package1;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyTest06 {
public static void main(String[] args) {
/** 要求: 线程调度的精准控制。
* 题目:多线程之间按顺序调用,实现 A->B->C
* 三个线程启动,要求如下:
* AA 打印5次,BB 打印5次。CC打印5次,依次循环
* 重点:标志位
*/
MyData06 myData06 = new MyData06();
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
myData06.printA();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "线程A").start();
new Thread(() -> {
for (int x = 1; x <= 10; x++) {
try {
myData06.printB();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "线程B").start();
new Thread(() -> {
for (int x = 1; x <= 10; x++) {
try {
myData06.printC();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "线程C").start();
}
}
class MyData06 {
// 这里创建一个 标志位,通过标志位来通知线程通信的顺序规则
private int num = 1;
private Lock lock = new ReentrantLock();
private Condition cA = lock.newCondition();
private Condition cB = lock.newCondition();
private Condition cC = lock.newCondition();
public void printA() throws InterruptedException {
// 获取锁
lock.lock();
while (num != 1) {
// 如果不满足条件就 释放锁并进入阻塞状态
cA.await();
}
try {
// 业务代码
System.out.println("======================================");
for (int x = 1; x <= 5; x++) {
System.out.println(Thread.currentThread().getName() + x);
}
// 改变标志位,依次让 printB() 方法可以执行。
num = 2;
// 唤醒cB队列内的阻塞线程
cB.signalAll();
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
// 释放锁
lock.unlock();
}
}
public void printB() throws InterruptedException {
lock.lock();
// 在printA()执行后就会把 num = 2,并且唤醒了 cB 队列内的阻塞队列,也就是被阻塞在这个地方的队列。
// 所以printA()执行完后,cB队列内阻塞的线程被printA()方法内的 cB.singalAll()唤醒,然后重新获取锁,此时 num 等于2,所以会跳过
// while判断,然后向下执行printB()内的业务代码。 然后依次类推,就可以实现 printA() -> printB() -> printC() -> printA()的顺序了
while (num != 2) {
cB.await();
}
try {
for (int x = 1; x <= 5; x++) {
System.out.println(Thread.currentThread().getName() + x);
}
num = 3;
cC.signalAll();
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
lock.unlock();
}
}
public void printC() throws InterruptedException {
lock.lock();
while (num != 3) {
cC.await();
}
try {
for (int x = 1; x <= 5; x++) {
System.out.println(Thread.currentThread().getName() + x);
}
num = 1;
cA.signalAll();
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
lock.unlock();
}
}
}
5. 8锁现象
这里8锁实际上就是 synchronized 字段在不同情况下的具体实现方式。这里具体要搞清楚不同情况下 synchronized 到底锁的是什么。下面就分别举出例子进行分析。
① 情况一
package com.llh.juc.eightlock;
public class Lock1 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(()->{phone.sendEmail();}).start();
Thread.sleep(1000);
new Thread(()->{phone.sendSMS();}).start();
}
}
class Phone{
public synchronized void sendEmail() {
System.out.println("sendEmail");
}
public synchronized void sendSMS(){
System.out.println("sendSMS");
}
}
分析: 先打印出 sendEmail。
因为这里使用 synchronized 来修饰两个方法 sendEmail、sendSMS。通过 synchronized 来修饰方法的话,那么synchronized锁的对象就是 调用当前方法的类的实例对象。
又因为两个子线程中调用的是同一个实例对象phone,所以这两个线程就是 争抢同一把锁,所以这里就是那个线程先执行到获取锁的位置(这里指的就是被 synchronized 修饰的方法),锁就先给谁。
② 情况二
package com.llh.juc.eightlock;
import java.util.concurrent.TimeUnit;
public class Lock2 {
public static void main(String[] args) throws InterruptedException {
Phone2 phone2 = new Phone2();
new Thread(() -> {
try {
phone2.sendEmail();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"A").start();
// 通过让主线程阻塞来达到上边的线程先运行的目的。
Thread.sleep(2000);
new Thread(()->{
try {
phone2.sendSMS();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"B").start();
}
}
class Phone2{
public synchronized void sendEmail() throws InterruptedException {
// 因为这里是 sleep() 方法,所以该线程不会因为阻塞就释放自己的锁和资源。
TimeUnit.SECONDS.sleep(1);
/**
* 如果这里使用的是 wait()方法,那么该线程A就会释放自己所拥有的锁,那么轮到线程B后,线程B就会获得锁,然后执行。
然后线程A苏醒后再拿到锁在执行SendEmail方法所以这时两个线程的执行流程就是
A(获取到锁,但是因为wait()又释放了对于Phone2对象的锁) -> B(获得了对Phone2对象的锁,并执行了sendSMS()方法)
-> A(重新获得Phone2对象的锁,然后执行代码)
*/
// this.wait(3000);
System.out.println("sendEmail");
}
public synchronized void sendSMS() throws InterruptedException {
System.out.println("sendSMS");
}
}
分析:先打印出 sendEmail
同情况一,这里 synchronized 锁的还是调用该方法的类的实例对象。main方法中两个子线程中调用的是同一个实例对象 phone2,所以两个子线程争抢的是同一把锁。
sendEmail() 方法中有一个
sleep(1)
的方法,但是我们知道线程因为 sleep() 而被阻塞是不会 释放自己拥有的锁以及资源,所以这时虽然被阻塞,但是其它线程同样拿不到锁。所以这里仍然是 线程A 执行完后 线程B才拿到锁,然后执行。如果sendEmail()方法中的不是
sleep(1)
而是this.wait(int time)
方法的话,那么线程因为 wait() 被阻塞就会释放自己的锁和资源。所以这时在main方法中,虽然线程A先拿到锁,但是由于sendEmail()方法中的wait()会释放出自己拿到的锁,那么线程B这时就会把锁抢过来然后执行自己的代码,线程B执行完后把锁释放后线程A才能重新获取并执行。所以这种情况的输出结果是: 先输出 sendSMS 然后才是 sendEmail
③ 情况三
package com.llh.juc.eightlock;
public class Lock3 {
public static void main(String[] args) throws InterruptedException {
/** 这里因为hello()方法没有被synchronized方法修饰,所以这个方法不是同步方法,不受锁的影响不用去获取锁。
* A、B 线程共用一把锁(对Phone3对象的锁) ,所以这里A、B线程需要进行抢夺锁
*/
Phone3 phone3 = new Phone3();
new Thread(()->{
try {
phone3.sendEmail();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"A").start();
// 让main线程阻塞2s,保证线程A先执行
Thread.sleep(2000);
new Thread(()->{phone3.sendSMS();},"B").start();
new Thread(phone3::hello,"C").start();
}
}
class Phone3{
public synchronized void sendEmail() throws InterruptedException {
Thread.sleep(3000);
// this.wait(3000);
System.out.println("sendEmail");
}
public synchronized void sendSMS(){
System.out.println("sendSMS");
}
// 该方法没有被synchronized修饰,所以不需要线程同步。
public void hello(){
System.out.println("hello world!");
}
}
分析: 输出结果为 hello world -> sendEmail -> sendSMS
这里同情况1、2, 线程 A、B 争抢的还是同一把锁,但是 hello()方法因为没有被 synchronized 修饰,所以线程C不需要争抢锁。
所以在main方法中 线程A先执行并拿到锁,然后线程A被sendEmail()方法体中的 sleep(3000)阻塞了3s。main线程被 sleep(2000) 阻塞两秒后 轮到线程B、C 开始执行,线程B因为需要争抢锁(此时锁被线程A抢占)所以被阻塞,线程C因为不需要抢占锁所以直接执行完,然后是线程 A -> 线程B。
④ 情况四
package com.llh.juc.eightlock;
public class Lock4 {
public static void main(String[] args) throws InterruptedException {
Phone4 p1 = new Phone4();
Phone4 p2 = new Phone4();
/** 因为synchronized修饰的方法所用对象 是 调用该方法的对象。
* 所以这里 线程A、B中方法的调用者不同,一个是p1、一个是p2。所以这两个线程争夺的不是同一个锁,所以这里线程B就不要等待线程A释放锁。
* 所以这里就是线程B先执行完。
*/
new Thread(() -> {
try {
p1.sendEmail();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"A").start();
Thread.sleep(2000);
new Thread(() -> {
p2.sendSMS();
},"B").start();
}
}
class Phone4{
public synchronized void sendEmail() throws InterruptedException {
Thread.sleep(4000);
System.out.println("sendEmail");
}
public synchronized void sendSMS(){
System.out.println("sendSMS");
}
}
分析: 打印顺序为 sendSMS -> sendEmail
因为这里synchronized修饰的方法,所以锁的对象是调用该方法的实例对象。
main方法中 线程A、B中使用的是不同的实例对象,分别是 p1、p2。所以这里两个线程争抢的不是同一把锁,线程A需要的锁是锁p1对象的,线程B需要的锁是锁p2对象的。所以这里两个线程之间的锁不冲突,所以就各自执行各自的。那么因为线程A被阻塞的时间过长,所以线程B先执行完成。
⑤ 情况五
package com.llh.juc.eightlock;
public class Lock5 {
public static void main(String[] args) throws InterruptedException {
/** 如果一个方法同时被 synchronized 和 static 修饰的话,那么synchronized所的对象就是 该类的Class对象。
* 而一个类只有一个Class类型对象,所以这里线程 A、B 争抢的是同一把锁。所以这里线程B要在线程A执行完后释放了锁,线程B再执行。
*/
Phone5 p1 = new Phone5();
new Thread(() -> {
try {
p1.sendEmail();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"A").start();
Thread.sleep(2000);
new Thread(() -> {
p1.sendSMS();
},"B").start();
}
}
class Phone5{
public static synchronized void sendEmail() throws InterruptedException {
Thread.sleep(4000);
System.out.println("sendEmail");
}
public static synchronized void sendSMS(){
// public synchronized void sendSMS(){
System.out.println("sendSMS");
}
}
分析:打印顺序为 sendEmail -> sendSMS
一个方法如果被 static、synchronized 同时修饰的话,那么这时synchronized锁的对象是就是调用该方法的类的class对象。
所以这里线程A、B 争抢的都是对 Phone类 的class对象的锁,因为一个类只有一个class对象,所以这里两个线程是争抢的同一把锁,所以这里就要 线程A 先执行完后对 Phone类的class对象的锁释放,然后线程B才可以拿到锁再执行自己的代码。
⑥ 情况六
package com.llh.juc.eightlock;
public class Lock6 {
public static void main(String[] args) throws InterruptedException {
/** 如果一个方法同时被 synchronized 和 static 修饰的话,那么synchronized所的对象就是 该类的Class对象。
* 而一个类只有一个Class类型对象,所以这里线程 A、B 争抢的是同一把锁。所以这里线程B要在线程A执行完后释放了锁,线程B再执行。
*
* 因为这里锁的是Phone6 类的Class对象,所以这里不管有几个phone6对象实例,这些被 synchronized和static 修饰的方法争抢的都是同一把锁(对于Phone6的Class对象的锁)
*/
Phone5 p1 = new Phone5();
Phone5 p2 = new Phone5();
new Thread(() -> {
try {
p1.sendEmail();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"A").start();
Thread.sleep(2000);
new Thread(() -> {
p2.sendSMS();
},"B").start();
}
}
class Phone6{
public static synchronized void sendEmail() throws InterruptedException {
Thread.sleep(4000);
System.out.println("sendEmail");
}
public static synchronized void sendSMS(){
// public synchronized void sendSMS(){
System.out.println("sendSMS");
}
}
分析: 输出顺序为 sendEmail -> sendSMS
同情况五中的分析,调用static synchronized 修饰的方法 争抢的是调用该方法的类的class对象。所以这里 线程A、B 争抢的还是一把锁。
虽然这里线程A、B中使用不同的 Phone6的实例对象来调用对应的方法,但是一个类只有一个class对象,所以这里 p1、p2 指向的都是同一个class对象,所以这种情况跟方法是由 哪个实例对象调用的无关。所以线程B依旧要等待线程A释放锁。
⑦ 情况七
package com.llh.juc.eightlock;
public class Lock7 {
public static void main(String[] args) throws InterruptedException {
/** 这里 sendEmail方法被 synchronized和static修饰,所以这个方法的synchronized锁的是 Phone7类的Class对象,而 sendSMS方法只被 synchronized 修饰,
* 所以锁的是 调用该方法的对象(也就说是 Phone7 的对象实例),所以线程A、B竞争的不是同一把锁。
*/
Phone7 p = new Phone7();
new Thread(()->{
try {
p.sendEmail();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"A").start();
Thread.sleep(3000);
new Thread(()->{
p.sendSMS();
},"B").start();
}
}
class Phone7{
public static synchronized void sendEmail() throws InterruptedException {
Thread.sleep(4000);
System.out.println("sendEmail");
}
public synchronized void sendSMS(){
System.out.println("sendSMS");
}
}
分析: 输出顺序为 sendSMS -> sendEmail
这里 sendEmail() 被 static synchronized 修饰,所以执行该方法 锁的是 调用该方法的对象的 类的class对象。而 sendSMS 被 synchronized修饰,所以执行该方法 锁的是 调用该方法的 类的实例对象。
所以main方法中,线程A、B争抢的就不是同一把锁,所以线程B不需要等待线程A。
⑧ 情况八
package com.llh.juc.eightlock;
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
// 这里的原理同 Lock7中,这里不做多叙述
Phone8 p1 = new Phone8();
Phone8 p2 = new Phone8();
new Thread(() -> {
try {
p1.sendEmail();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"A").start();
Thread.sleep(3000);
new Thread(() -> {
p2.sendSMS();
},"B").start();
}
}
class Phone8{
public static synchronized void sendEmail() throws InterruptedException {
Thread.sleep(4000);
System.out.println("sendEmail");
}
public synchronized void sendSMS(){
System.out.println("sendSMS");
}
}
分析: 顺序为 sendSMS --> sendEmail
跟情况7同理,不做过多赘述。
8锁总结:对于上述的情况的主要分析思路以及重点
synchronized修饰的方法,那么锁的对象就是 调用该方法的类的实例对象。
static synchronized修饰的方法,那么锁的对象就是 调用该方法的类的class对象
sleep(int time)、wait(int time) 都会让调用方法的线程进入阻塞状态。 但是 sleep() 类型导致线程阻塞,线程不需要 释放锁拥有的锁和资源。wait()导致线程阻塞,线程需要释放所用的锁 和 资源。
同时还要知道 一个类 最多只有一个 该类的class对象,可以有多个 该类的实例对象。(这个不清楚看一下 JVM 加载类的流程)
从JVM对类的加载流程可以看出来 一个类 只有一个该类的Class对象,可以有多个该类的实例对象。 class对象是用来给类实例化对象使用的(class对象作为模板)。并且每一个实例对象都指向同一个(唯一)的该类的 class对象。
6 安全集合类
我们以前使用的关于集合的类在单线程下操作是安全的,但是在多线程并发中就会使线程不安全的了。所以这里我们需要寻找出多线程下集合线程安全的解决方法。
6.1 关于ArrayList线程不安全
举例多线程下集合不安全的例子
package com.llh.juc.mytest;
import java.util.ArrayList;
public class Test1 {
public static void main(String[] args) throws InterruptedException {
ArrayList list = new ArrayList();
for (int i = 1; i <= 10; i++) {
int finalI = i;
new Thread(() -> {
for (int j = 1; j <= 1000; j++){
list.add(finalI + "<-->" +j);
}
}).start();
}
while (Thread.activeCount() > 2){
}
System.out.println(list.size());
}
}
// 如果操作是线程安全的话,那么最会输出值会是 10000。但是最后输出值为 7321 左右
// 说明ArrayList在多线程下线程不安全。
测试代码二
package com.llh.juc.mytest;
import java.util.ArrayList;
import java.util.Random;
public class Test2 {
public static void main(String[] args) {
ArrayList list = new ArrayList();
for (int i = 1; i<=50;i++){
new Thread(() -> {
list.add(new Random().nextInt(100));
System.out.println(list);
}).start();
}
System.out.println(list.size());
}
}
// 执行后抛出Exception in thread "Thread-36" java.util.ConcurrentModificationException 异常。
// 这个异常的意思是 同时发生修改异常。说明多个线程在同一时刻修改了同一个索引中的元素
上边的线程不安全原因是因为没有给ArrayList对应的 add、remove等方法加锁。虽然上边的 ArrayList是线程不安全的,但是vector是线程安全的,但是vector的线程安全是依赖于使用 synchronized 来实现的。
解决方案: 使用CopyOnWriteArrayList 来替代 ArrayList
package com.llh.juc.mytest;
import java.util.concurrent.CopyOnWriteArrayList;
public class Test3 {
public static void main(String[] args) {
CopyOnWriteArrayList list = new CopyOnWriteArrayList();
for (int i = 1; i <= 30; i++) {
list.add(i);
System.out.println(list);
}
while (Thread.activeCount() > 2) {
}
System.out.println(list.get(0));
}
}
// 运行后没有抛出异常,并且结果正确。
原因分析:
写入时复制(CopyOnWrite,简称COW)思想是计算机程序设计领域中的一种优化策略。
其核心思想是,如果有多个调用者(Callers)同时要求相同的资源(如内存或者是磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源内容时,系统才会真正复制一份专用副本(private copy)给该调用者,修改者现在副本上进行修改,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。
此做法主要的优点是如果调用者没有修改资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。读写分离,写时复制出一个新的数组,完成插入、修改或者移除操作后将新数组赋值给老数组。
CopyOnWriteArrayList为什么并发安全且性能比Vector好?
我知道 Vector是增删改查方法都加了synchronized,保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降,而CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性能就好于Vector,CopyOnWriteArrayList支持读多写少的并发情况。
简单来说,COW是读写分离的。多个线程的读操作可以同时访问系统资源,如果一个线程要进行写操作的话,就需要获取写锁(让同一时刻只能有一个线程在进行更改操作),然后系统会复制一份资源的副本给该线程(现在副本上进行修改),在此期间其它线程的读操作是不受影响的,写线程在副本写入完成后把副本复制到原来那个资源,这样就完成了写操作,然后释放出写锁。
6.2 关于HashSet线程不安全
同样HashSet也是线程不安全的,从而也就表示了HashMap也是线程不安全的。因为HashSet底层用的就是HashMap。那么 HashSet的多线程替代解决方法就是使用 CopyOnWriteHashSet 。
package com.llh.juc.unsafe_collection;
import java.util.concurrent.CopyOnWriteArraySet;
public class COWSetTest {
public static void main(String[] args) throws InterruptedException {
// 多线程下使用 CopyOnWriteHashSet 来替代HashSet从而实现 多线程下的线程安全。
CopyOnWriteArraySet set = new CopyOnWriteArraySet();
Thread t1 = new Thread(() -> {
for (int i = 1; i <= 1000; i++) {
set.add(i);
}
});
Thread t2 = new Thread(() -> {
for (int i = 1001; i <= 2000; i++) {
set.add(i);
}
});
Thread t3 = new Thread(() -> {
for (int i = 2001; i <= 3000; i++) {
set.add(i);
}
});
// Thread.join() 方法表示 等待该线程结束。这里是为了让main线程等待三个子线程。不然子线程还没执行完 main 线程就结束退出了。
t1.start();t1.join();
t2.start();t2.join();
t3.start();t3.join();
System.out.println(set.size());
}
}
HashMap也是多线程下线程不安全的。这里对于 HashMap 的多线程替代方法就是 ConcurrentHashMap()。具体代码就不演示了。
7 Callable
JDK中关于 Callable 的介绍如下:
Callable 和 Runnable 的区别?
Callable有返回值并且可以抛出异常。两个接口需要实现的方法不一样,Callable接口需要实现call()方法,Runnable需要实现run()方法。
Callable接口的具体代码如下:
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
// 可以看出实现这个方法必须要有一个返回值,并且该接口的实现类可以抛出异常。
V call() throws Exception;
}
Callable的使用方式:
我们通常使用 new Thread(Runnable) 来创建一个新线程,但是这里需要的参数是 Runnable ,那么需要想办法把 Callable 和 Runnable练习起来。而 Runnable有一个实现类 FutureTask,FutureTask的构造器为 FutureTask(Callable),所以这样就联系起来了。
new Thread(new FutureTask(new Callable)) 这样来使用
Callable的简单使用:
package com.llh.juc.mycallable;
import java.util.concurrent.FutureTask;
public class CallableTest1 {
public static void main(String[] args) {
FutureTask future = new FutureTask(() -> {
System.out.println("实现了callable的call方法,并返回了666");
return 666;
});
new Thread(future).start();
while (Thread.activeCount() > 2) {}
}
}
package com.llh.juc.package1;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask futureTask = new FutureTask(() -> {
System.out.println(Thread.currentThread().getName() + " call 被调用");
Thread.sleep(3000);
return 1024;
});
// FutureTask futureTask1 = new FutureTask(() -> {
// System.out.println(Thread.currentThread().getName() + " call 被调用");
// Thread.sleep(3000);
// return 1024;
// });
/** 一个FutureTask对象只能被执行一次,所以这个地方只有线程A会被执行。
* 如果想要对callable执行多次的话,那么就需要创建多个FutureTask对象。
* Callable 相较于 Runnable来说,Callable可以获取到线程执行代码的返回值 并且可以进行异常抛出。
*/
new Thread(futureTask,"A").start();
Thread.sleep(3000);
new Thread(futureTask,"B").start();
System.out.println(Thread.currentThread().getName() + "ok");
Integer num = (Integer) futureTask.get();
System.out.println(num);
}
}
8 辅助类
8.1 CountDownLatch
CountDownLatch 在JDK开发帮助手册中的介绍如下
CountDownLatch
是 Java 并发包(java.util.concurrent
)中的一个同步工具类,用于协调多个线程之间的同步。它允许一个或多个线程等待其他线程完成操作后再继续执行。
思路分析:
CountDownLatch(int num) 这个num就表示有多少个线程就要被 countdownlatch 进行控制。
线程完成指定的操作后执行 cntDowLatch.countDown()方法表示自己以完成指定的操作,同时计数器会进行-1操作
,通过 countDownLatch.await()来阻塞自己从而等待未完成操作的其它线程
当倒计数器的count被减为0,那么就表示当前操作所有线程都全部完成,所以就不需要阻塞了。这是 因为 await() 方法被阻塞的线程就会被释放并接着执行自己的代码。
代码演示:
package com.llh.juc.assistant_object;
import java.util.concurrent.CountDownLatch;
public class CountDownLatchTest2 {
public static void main(String[] args) throws InterruptedException {
// 计数器内的数字为2,表示有两个线程需要被 countdownlatch 管理
CountDownLatch countDownLatch = new CountDownLatch(2);
new Thread(() -> {
System.out.println("我是线程1");
// 执行完规定的代码,然后对计数器进行 -1 操作。
countDownLatch.countDown();
System.out.println("我已完成任务,调用 countDownLatch.await() 来阻塞自己,由此等待线程2完成任务。");
try {
// 调用 await() 来等待未完成指定任务的线程
countDownLatch.await();
System.out.println("线程2也完成任务了");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
Thread.sleep(3000);
new Thread(() -> {
System.out.println("我是线程2");
// 同理,对计数器进行-1。因为计数器的初始值为2,所以到此计数器内的值已经变为0,所以被 await() 阻塞的线程就会被释放。
countDownLatch.countDown();
System.out.println("我已完成任务");
}).start();
}
}
8.2 CyclicBarrier
帮助文档介绍
作用类似于 CountDownLatch ,不过CyclicBarrier可以被循环使用。
package com.llh.juc.assistant_object;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
System.out.println("召唤神龙成功");
});
for (int i = 1; i <= 7; i++){
int finalI = i;
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "收集了第" + finalI + "颗龙珠");
try {
// 将当前线程加入 cyclicBarrier(parties++) 并阻塞等待,直到 parties == 7 然后释放线程,并执行 cyclicBarrier中的跳闸命令
cyclicBarrier.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
}
}).start();
}
// CyclicBarrier 可以被循环使用。所以下边就是cyclicBarrier的第二次使用
for (int i = 1; i <= 7; i++){
int finalI = i;
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "收集了第" + finalI + "颗龙珠");
try {
// 将当前线程加入 cyclicBarrier(parties++) 并阻塞等待,直到 parties == 7 然后释放线程,并执行 cyclicBarrier中的跳闸命令
cyclicBarrier.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
}
}).start();
}
}
}