JUC学习笔记(一)

多线程进阶=》JUC并发编程

1.什么是juc

主要就是涉及到Java中三个包下的内容,分别是:

  • java.util.concurrent
  • java.util.concurrent.atomic
  • java.util.concurrent.Locks

传统的多线程使用Thread,并且通常是将Runable交给Thread去执行!

但Runable:没有返回值、效率比Callable低!

2.线程和进程

进程线程:

进程:一个程序,QQ.exe,即程序的集合!

一个进程往往包含多个线程,至少包含一个。

java默认有几个线程?答:两个,即main线程和GC线程!

对java而言,开启线程的三种方式:Thread、Runnable、Callable

**java真的能开启线程吗?**答:实际上是开不了的,因为它只能通过native方法去启动线程,即调用本地方法接口来完成!

并发并行:

并发:CPU的一个核上多个任务交替执行!

并行:CPU多核,多个线程可以同时执行!

public static void main(String[] args) {
    //获取CPU的核数
    //Cpu密集型、IO密集型
    System.out.println(Runtime.getRuntime().availableProcessors());
}

并发编程的本质:充分利用CPU的资源!

线程有几个状态:

java代码中显示6种:Thread.State

    public enum State {
        /**
         * Thread state for a thread which has not yet started.
         */
        //新生
        NEW,

        /**
         * Thread state for a runnable thread.  
         */
        //运行
        RUNNABLE,

        /**
         * Thread state for a thread blocked waiting for a monitor lock.
         */
        //阻塞
        BLOCKED,

        /**
         * Thread state for a waiting thread.
         */
        //等待
        WAITING,

        /**
         * Thread state for a waiting thread with a specified waiting time.
         */
        //超时等待
        TIMED_WAITING,

        /**
         * Thread state for a terminated thread.
         * The thread has completed execution.
         */
        //终止
        TERMINATED;
    }
wait/sleep区别:
1.来自不同的类

wait=>Object类

sleep=>Thread类

企业级开发中,线程休眠一般不用Thread.sleep()方法,而是使用:

import java.util.concurrent.TimeUnit;//仍然是在juc包下
//睡一天
TimeUnit.DAYS.sleep(1);
2.关于锁的释放

wait会释放锁,sleep不会释放锁!

3.使用的范围是不同的

wait必须在同步代码块中使用!

sleep可以在任何地方睡!

4.是否需要捕获异常

wait不需要捕获异常!wait其实也需要捕获异常,因为所有的线程都存在InterruptedException异常

sleep必须捕获异常(InterruptedException)

3.Lock锁(重点)

传统:Synchornized
/**
 * 真正的企业级开发不会使用下述的方式:即
 * 通过继承Runnable接口来定义一个线程类,比如:MyThread类
 * 然后重写MyThread类中的run()方法,因为其耦合性太强了
 */
public class SaleTicketDemo01 {
    public static void main(String[] args) {
        //开启一个线程来执行任务
        new Thread(new MyThread()).start();
    }
}
class MyThread implements Runnable{

    @Override
    public void run() {
        //todo
    }
}

//建议使用的方式是单独定义资源类,OOP思想
public class SaleTicketDemo01 {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        //lambda表达式,即:()->{}
        new Thread(()->{
            for (int i=0; i<40; i++){
                ticket.sale();
            }
        },"A").start();
        new Thread(()->{
            for (int i=0; i<30; i++){
                ticket.sale();
            }
        },"B").start();
        new Thread(()->{
            for (int i=0; i<20; i++){
                ticket.sale();
            }
        },"C").start();
    }
}
//建议使用的方式是单独定义资源类
class Ticket {
    private int ticketNum = 50;

    public void sale(){
        if(ticketNum > 0){
            System.out.println("剩余票数:"+ticketNum);
        }
    }
}

打印出的结果:
剩余票数:48
剩余票数:46
剩余票数:45
剩余票数:44
剩余票数:43
剩余票数:42
剩余票数:41
剩余票数:40
剩余票数:47
剩余票数:38
剩余票数:37
剩余票数:36
剩余票数:35
剩余票数:34
剩余票数:33
剩余票数:32
剩余票数:31
剩余票数:30
剩余票数:49
剩余票数:28
剩余票数:29
剩余票数:39
剩余票数:26
剩余票数:27
剩余票数:24
剩余票数:25
剩余票数:22
剩余票数:23
剩余票数:20
剩余票数:21
剩余票数:18
剩余票数:19
剩余票数:16
剩余票数:17
剩余票数:14
剩余票数:15
剩余票数:12
剩余票数:11
剩余票数:13
剩余票数:9
剩余票数:10
剩余票数:7
剩余票数:6
剩余票数:5
剩余票数:4
剩余票数:3
剩余票数:2
剩余票数:1
剩余票数:0
剩余票数:8

进程已结束,退出代码 0

//如何解决上述问题;
//最传统的方式是加synchornized
class Ticket {
    private int ticketNum = 50;

    //锁:锁两个东西,即对象和Class
    synchronized public void sale(){
        if(ticketNum > 0){
            System.out.println("剩余票数:"+--ticketNum);
        }
    }
}
输出结果为;
剩余票数:49
剩余票数:48
剩余票数:47
剩余票数:46
剩余票数:45
剩余票数:44
剩余票数:43
剩余票数:42
剩余票数:41
剩余票数:40
剩余票数:39
剩余票数:38
剩余票数:37
剩余票数:36
剩余票数:35
剩余票数:34
剩余票数:33
剩余票数:32
剩余票数:31
剩余票数:30
剩余票数:29
剩余票数:28
剩余票数:27
剩余票数:26
剩余票数:25
剩余票数:24
剩余票数:23
剩余票数:22
剩余票数:21
剩余票数:20
剩余票数:19
剩余票数:18
剩余票数:17
剩余票数:16
剩余票数:15
剩余票数:14
剩余票数:13
剩余票数:12
剩余票数:11
剩余票数:10
剩余票数:9
剩余票数:8
剩余票数:7
剩余票数:6
剩余票数:5
剩余票数:4
剩余票数:3
剩余票数:2
剩余票数:1
剩余票数:0
Lock的使用

Lock是一个interface,它有三个实现类:ReentrantLock(常用:可重入锁), ReentrantReadWriteLock.ReadLock(读锁), ReentrantReadWriteLock.WriteLock(写锁)

//Lock接口的方法:
void	lock();//加锁
void	unlock();//解锁
boolean	tryLock();//尝试获取锁
Condition	newCondition();//返回与当前锁实例相绑定的Condition实例

在这里插入图片描述

公平锁:十分公平,遵循先来后到(比如两个线程,一个3s可以执行完,另一个需要3h,而3h的线程排在3s线程前面,那么后面一个线程必须等待前面3h的线程执行完毕才可获取锁!)

非公平锁:可以插队(ReentrantLock默认使用非公平锁,Synchornized也是默认使用非公平锁)

基本使用方式:

在这里插入图片描述

//使用Lock锁进行上述的卖票业务:也能正确实现卖票业务
public class SaleTicketDemo02 {
    public static void main(String[] args) {
        Ticket2 ticket = new Ticket2();
        //lambda表达式,即:()->{}
        new Thread(()->{
            for (int i=0; i<40; i++){
                ticket.sale();
            }
        },"A").start();
        new Thread(()->{
            for (int i=0; i<30; i++){
                ticket.sale();
            }
        },"B").start();
        new Thread(()->{
            for (int i=0; i<20; i++){
                ticket.sale();
            }
        },"C").start();
    }
}

/**
 * Lock三部曲
 * 1. new ReentrantLock();
 * 2. lock.lock();//加锁
 * 3. finally代码块lock.unlock();//解锁
 */
class Ticket2 {
    private int ticketNum = 50;

    Lock lock = new ReentrantLock();

    public void sale(){

        lock.lock();//加锁

        try {
            //业务代码
            if(ticketNum > 0){
                System.out.println("剩余票数:"+--ticketNum);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();//解锁
        }
    }
}
Synchornized锁与Lock锁的区别

1.Synchornized 内置的java关键字,Lock是一个java类

2.Synchornized无法判断获取锁的状态,Lock可以判断是否获取到了锁

3.Synchornized自动释放锁,Lock必须手动释放锁!如果不能释放锁,则会造成死锁!

4.Synchornized 假设现在有两个线程,线程1获取了锁,但阻塞了,那么线程2就会傻傻的等下去;Lock锁就不会一直等待下去,而是通过**tryLock**()去尝试获取锁

5.Synchornized 可重入锁,不可以中断的,非公平;Lock,可重入锁,可以判断锁,非公平(也可以自己设置成公平的)

6.Synchornized 适合锁少量的代码同步问题,Lock适合锁大量的同步代码!

锁是什么?如何判断锁的是谁?
Synchornized 对象锁和类锁的区别

参考:https://www.cnblogs.com/owenma/p/8609348.html

//对象锁
Synchornized (this){
    //todo
}
//或者任何对象obj
Object obj = new Object();
Synchornized (obj){
    //todo
}

//=================
//类锁
Synchornized (Ticket.class){
    //todo
}

4.生产者和消费者问题

面试的时候经常会考手写代码的笔试题目有:1.单例模式 2.排序算法 3.生产者和消费者 4.死锁(恰巧该笔记中所有问题都有)

生产者和消费者问题 Synchornized版

看测试代码:

/**
 * 线程之间的通信问题:生产者和消费者问题! 等待唤醒,通知唤醒
 * 线程交替执行,A和B两个线程,操作同一个变量  num=0
 * A:num+1 A线程执行num+1操作
 * B:num-1 B线程执行num-1操作
 */
public class TestA {
    public static void main(String[] args) {
        //线程操作资源类
        //new一个资源类
        Data data = new Data();

        new Thread(()->{
            for(int i=0; i<5; i++){
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        new Thread(()->{
            for(int i=0; i<5; i++){
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

    }
}

//资源类:将资源类与线程拆分
class Data {
    //数字
    private int num = 0;

    //执行+1操作
    public synchronized void increment() throws InterruptedException {
        //Lock三口诀:等待,业务,通知
        if(num!=0){
            //等待
            this.wait();
        }
        num++;

        //打印输出当前线程状态
        System.out.println(Thread.currentThread().getName()+"=>"+num);

        //接下来:通知B线程,我+1完毕了
        this.notifyAll();
    }

    //执行-1操作
    public synchronized void decrement() throws InterruptedException {
        //Lock三口诀:判断等待,业务,通知
        if(num==0){
            //等待
            this.wait();
        }
        num--;

        //打印输出当前线程状态
        System.out.println(Thread.currentThread().getName()+"=>"+num);

        //接下来:通知A线程,我-1完毕了
        this.notifyAll();
    }
}

//上述main方法执行返回结果为:
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
//很明显,完美的执行了生产者消费者的+1-1操作,即完成了基本的线程通信!
问题存在:上述代码只有两个线程,那面试管问:假设有A B C D 4个线程呢,会产生怎样的结果?

那么可能会蹦出一些奇奇怪怪的答案了:

A=>1
B=>0
C=>1
D=>0
C=>1
D=>0
C=>1
D=>0
C=>1
D=>0
C=>1
D=>0
B=>-1
B=>-2
B=>-3
B=>-4
A=>-3
那么如何解决该问题呢?

使用while循环判断。可以避免虚假唤醒问题!因为if判断进去之后不会停,而while判断进去后,一旦出现异常就会停止!

官方文档Object类下的wait()方法中的注意点:

在这里插入图片描述

public class TestA {
    public static void main(String[] args) {
        //线程操作资源类
        //new一个资源类
        Data data = new Data();

        new Thread(()->{
            for(int i=0; i<5; i++){
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        new Thread(()->{
            for(int i=0; i<5; i++){
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"B").start();

        new Thread(()->{
            for(int i=0; i<5; i++){
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"C").start();

        new Thread(()->{
            for(int i=0; i<5; i++){
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"D").start();

    }
}

//资源类:将资源类与线程拆分
class Data {
    //数字
    private int num = 0;

    //执行+1操作
    public synchronized void increment() throws InterruptedException {
        //Lock三口诀:等待,业务,通知
        while (num!=0){
            //等待
            this.wait();
        }
        num++;

        //打印输出当前线程状态
        System.out.println(Thread.currentThread().getName()+"=>"+num);

        //接下来:通知B线程,我+1完毕了
        this.notifyAll();
    }

    //执行-1操作
    public synchronized void decrement() throws InterruptedException {
        //Lock三口诀:判断等待,业务,通知
        while (num==0){
            //等待
            this.wait();
        }
        num--;

        //打印输出当前线程状态
        System.out.println(Thread.currentThread().getName()+"=>"+num);

        //接下来:通知A线程,我-1完毕了
        this.notifyAll();
    }
}

//之这一次打印输出结果为:
A=>1
B=>0
C=>1
B=>0
C=>1
B=>0
C=>1
B=>0
C=>1
B=>0
C=>1
D=>0
A=>1
D=>0
A=>1
D=>0
A=>1
D=>0
A=>1
D=>0

进程已结束,退出代码 0
JUC版的生产者和消费者问题

Synchornized中的wait()和notify()方法在JUC中则由Condition类来实现!

在这里插入图片描述

Condition类中的方法名为:await()和signal()。

即Condition取代了对象监视器方法的使用!

/**
 * 线程之间的通信问题:生产者和消费者问题! 等待唤醒,通知唤醒
 * 线程交替执行,A和B两个线程,操作同一个变量  num=0
 * A:num+1 A线程执行num+1操作
 * B:num-1 B线程执行num-1操作
 */
public class TestB {
    public static void main(String[] args) {
        //线程操作资源类
        //new一个资源类
        Data2 data = new Data2();

        new Thread(()->{
            for(int i=0; i<5; i++){
                data.increment();
            }
        },"A").start();

        new Thread(()->{
            for(int i=0; i<5; i++){
                data.decrement();
            }
        },"B").start();

        new Thread(()->{
            for(int i=0; i<5; i++){
                data.increment();
            }
        },"C").start();

        new Thread(()->{
            for(int i=0; i<5; i++){
                data.decrement();
            }
        },"D").start();

    }
}

//资源类:将资源类与线程拆分
class Data2 {
    //数字
    private int num = 0;

    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    //执行+1操作
    public void increment() {
        //获取锁
        lock.lock();

        //业务代码
        try {
            //Lock三口诀:等待,业务,通知
            while (num!=0){
                //等待
                condition.await();
            }
            num++;

            //打印输出当前线程状态
            System.out.println(Thread.currentThread().getName()+"=>"+num);

            //接下来:通知B线程,我+1完毕了
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放锁
            lock.unlock();
        }
    }

    //执行-1操作
    public void decrement() {
        //获取锁
        lock.lock();

        //业务代码
        try {
            //Lock三口诀:等待,业务,通知
            while (num==0){
                //等待
                condition.await();
            }
            num--;

            //打印输出当前线程状态
            System.out.println(Thread.currentThread().getName()+"=>"+num);

            //接下来:通知B线程,我+1完毕了
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放锁
            lock.unlock();
        }
    }
}
//运行main方法的执行结果为:
A=>1
B=>0
C=>1
D=>0
A=>1
B=>0
C=>1
D=>0
A=>1
B=>0
C=>1
D=>0
A=>1
B=>0
C=>1
D=>0
A=>1
B=>0
C=>1
D=>0
//没有问题!

Lock的作用覆盖了Synchornized的作用,Condition类的作用覆盖了Object类自带的wait()、notify()方法的作用!

在这里插入图片描述

任何一个新的技术,绝对不仅仅只是覆盖了原有的技术,一定有其优势和对原有技术的补充!

Condition的优势就是可以精准的通知和唤醒线程!

接下来测试一个A B C线程顺序执行的例子
/**
 * A执行完调用B,B执行完调用C,C执行完调用A
 */
public class TestC {
    public static void main(String[] args) {
        //线程操作资源类
        //new一个资源类
        Data3 data = new Data3();

        new Thread(()->{
            for(int i=0; i<5; i++){
                data.printA();
            }
        },"A").start();

        new Thread(()->{
            for(int i=0; i<5; i++){
                data.printB();
            }
        },"B").start();

        new Thread(()->{
            for(int i=0; i<5; i++){
                data.printC();
            }
        },"C").start();


    }
}

//资源类:将资源类与线程拆分
class Data3 {
    int num = 1;//假设num=1时让A执行,num=2时让B执行,num=1时让C执行

    private Lock lock = new ReentrantLock();
    //同步监视器厉害的地方在于一个同步监视可以一次只监视一个
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();

    public void printA(){
        lock.lock();
        try {
            //业务:判断 -> 执行 -> 通知
            while (num!=1){
                //不等于1,则执行等待
                condition1.await();
            }
            System.out.println(Thread.currentThread().getName()+":执行了AAAA线程!");

            num=2;
            //唤醒指定的线程,即B线程
            condition2.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printB(){
        lock.lock();
        try {
            //业务:判断 -> 执行 -> 通知
            while (num!=2){
                //不等于2,则执行等待
                condition2.await();
            }
            System.out.println(Thread.currentThread().getName()+":执行了BBBB线程!");

            num=3;
            //唤醒指定的线程,即C线程
            condition3.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printC(){
        lock.lock();
        try {
            //业务:判断 -> 执行 -> 通知
            while (num!=3){
                //不等于3,则执行等待
                condition3.await();
            }
            System.out.println(Thread.currentThread().getName()+":执行了CCCC线程!");

            num=1;
            //唤醒指定的线程,即A线程
            condition1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

//main方法打印输出结果为:
A:执行了AAAA线程!
B:执行了BBBB线程!
C:执行了CCCC线程!
A:执行了AAAA线程!
B:执行了BBBB线程!
C:执行了CCCC线程!
A:执行了AAAA线程!
B:执行了BBBB线程!
C:执行了CCCC线程!
A:执行了AAAA线程!
B:执行了BBBB线程!
C:执行了CCCC线程!
A:执行了AAAA线程!
B:执行了BBBB线程!
C:执行了CCCC线程!

上述多线程按照一定顺序执行的应用环境:下单 -> 支付 -> 交易 -> 物流

5、 8锁现象(锁是什么?如何判断锁的是谁?)

如何判断锁的是谁!永远的知道锁是什么?以及锁的是谁?

深刻理解我们的锁

问题1:标准情况下,两个线程是先打印"sendSms"还是"call"?测试结果是:首先打印"sendSms",然后"call"

问题2:sendSms()方法中延迟4s,两个线程是先打印"sendSms"还是"call"?还是先打印"sendSms"

答案问题1和问题2都是先打印"sendSms",这并不是因为A线程先执行,而是因为有锁的存在。因为被synchronized修饰的方法锁的是方法的调用者!即下方的phone对象,而锁只有一个,所以哪个方法先获得锁就会先执行那个方法!

package com.codeyu.lock8;

import java.util.concurrent.TimeUnit;

/**
 * 8锁:就是关于锁的8个问题
 * 1. 标准情况下,两个线程是先打印"sendSms"还是"call"?测试结果是:首先打印"sendSms",然后"call"
 * 2. sendSms()方法中延迟4s,两个线程是先打印"sendSms"还是"call"?还是先打印"sendSms"
 * 答案:问题1和问题2都是先打印"sendSms",这并不是因为A线程先执行,而是因为有锁的存在。因为被synchronized
 * 修饰的方法锁的是方法的调用者!即下方的phone对象,而锁只有一个,所以哪个方法先获得锁就会先执行那个方法!
 */
public class Test1 {

    public static void main(String[] args) {
        Phone phone = new Phone();

        new Thread(()->{
            phone.sendSms();
        },"A").start();

        //捕获
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            phone.call();
        },"B").start();
    }
}

class Phone{
    public synchronized void sendSms(){
        //捕获
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sendSms");
    }

    public synchronized void call(){
        System.out.println("call");
    }
}

问题3:增加了一个没有用synchronized修饰的普通方法hello()后,执行下述main方法是先打印"sendSms"还是先打印"hello"?

答案此处是先打印"hello",因为hello()方法没有用synchronized修饰,它不是一个同步方法。

问题4:两个对象,分别执行两个同步方法,是先打印"sendSms"还是先打印"call"?

答案此处是先打印"call",因为此处有两个对象phone1和phone2,两个线程分别通过不同的对象调用相应的方法,不存在争抢同一个锁的问题!即两个对象,两个调用者,两把锁!

/**
 * 8锁:就是关于锁的8个问题
 * 问题3:增加了一个没有用synchronized修饰的普通方法hello()后,执行下述main方法是先打印"sendSms"还是先打印"hello"?
 * 答案:此处是先打印"hello",因为hello()方法没有用synchronized修饰,它不是一个同步方法。
 *
 * 问题4:两个对象,分别执行两个同步方法,是先打印"sendSms"还是先打印"call"?
 * 答案:此处是先打印"call",因为此处有两个对象phone1和phone2,两个线程分别通过不同的对象调用相应
 * 的方法,不存在争抢同一个锁的问题!即两个对象,两个调用者,两把锁!
 */
 //问题3的Test类
public class Test2 {

    public static void main(String[] args) {
        Phone2 phone = new Phone2();

        new Thread(()->{
            phone.sendSms();
        },"A").start();

        //捕获
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            phone.hello();
        },"B").start();
    }
}
//问题4的Test类
public class Test2 {

    public static void main(String[] args) {
        //new 两个对象
        Phone2 phone1 = new Phone2();
        Phone2 phone2 = new Phone2();

        new Thread(()->{
            phone1.sendSms();
        },"A").start();

        //捕获
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            phone2.call();
        },"B").start();
    }
}

class Phone2{
    //synchronized修饰的方法锁的是方法的调用者,即锁的是对象
    public synchronized void sendSms(){
        //捕获
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sendSms");
    }

    public synchronized void call(){
        System.out.println("call");
    }

    public void hello(){
        System.out.println("hello");
    }
}

问题5:将sendSms()方法和call()方法都设置为静态的同步方法,且main方法中只new一个对象,问是先打印"sendSms"还是先打印"call"?

答案此处仍然是先打印"sendSms",和问题1/2的答案一致,但此处与问题1/2的原因并不一致!问题1/2先打印"sendSms"是两个方法都被synchronized修饰,而调用sendSms()方法和call()方法的对象又只有一个。而问题5中两个方法变为了static方法,一个对象分别调用两个方法,此处不是锁的phone对象,而是锁的Class对象,而Class对象全局唯一!

问题6:仍将sendSms()方法和call()方法都设置为静态的同步方法,但这一次new两个对象,且在两个线程中分别用不同的对象调用不同的方法,问是先打印"sendSms"还是先打印"call"?

答案仍然是先打印"sendSms",因为static同步方法锁的是Class对象,而Class对象全局唯一,所以先执行哪个方法就会先输出该方法对应的信息!而sendSms()先获得锁!

package com.codeyu.lock8;

import java.util.concurrent.TimeUnit;

/**
 * 8锁:就是关于锁的8个问题
 * 问题5:将sendSms()方法和call()方法都设置为静态的同步方法,且main方法中只new一个对象,
 * 问是先打印"sendSms"还是先打印"call"?
 *
 * 答案:此处仍然是先打印"sendSms",和问题1/2的答案一致,但此处与问题1/2的原因并不一致!
 * 问题1/2先打印"sendSms"是两个方法都被synchronized修饰,而调用sendSms()方法和call()方法
 * 的对象又只有一个。而问题5中两个方法变为了static方法,一个对象分别调用两个方法,此处不是锁的phone对象,
 * 而是锁的Class对象,而Class对象全局唯一!
 *
 * 问题6:仍将sendSms()方法和call()方法都设置为静态的同步方法,但这一次new两个对象,且在两个线程中
 * 分别用不同的对象调用不同的方法,问是先打印"sendSms"还是先打印"call"?
 *
 * 答案:仍然是先打印"sendSms",因为static同步方法锁的是Class对象,而Class对象全局唯一,所以先执行哪个方法就会
 * 先输出该方法对应的信息!而sendSms()先获得锁!
 */
//问题5的Test类
public class Test3 {

    public static void main(String[] args) {
        //new一个对象
        Phone3 phone = new Phone3();

        new Thread(()->{
            phone.sendSms();
        },"A").start();

        //捕获
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            phone.call();
        },"B").start();
    }
}
//问题6的Test类
public class Test3 {

    public static void main(String[] args) {
        //new一个/两个对象
        Phone3 phone1 = new Phone3();
        Phone3 phone2 = new Phone3();

        new Thread(()->{
            phone1.sendSms();
        },"A").start();

        //捕获
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            phone2.call();
        },"B").start();
    }
}

//Phone3只有唯一的一个Class对象
//即:Class<Phone3> phone3Class = Phone3.class; 全局唯一
class Phone3{
    //synchronized修饰的方法锁的是方法的调用者,即锁的是对象
    //static 静态方法
    //类一加载就有了!锁的是Class
    public static synchronized void sendSms(){
        //捕获
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sendSms");
    }

    public static synchronized void call(){
        System.out.println("call");
    }
}

问题7:将call()方法都设置非静态的同步方法,而sendSms()为静态同步方法,且main方法中只new一个对象,问是先打印"sendSms"还是先打印"call"?

答案:先打印"call",因为sendSms()方法锁的是Class模板,而call()方法锁的是调用者,二者锁的不是同一个东西所以先打印"call"!

问题8:同样,call()为非静态的同步方法,sendSms()为静态同步方法,main方法中new两个对象,分别调用call()方法和sendSms()方法,问先打印什么?

答案先打印call",理由同问题7的理由!

package com.codeyu.lock8;

import java.util.concurrent.TimeUnit;

/**
 * 8锁:就是关于锁的8个问题
 * 问题7:将call()方法都设置非静态的同步方法,而sendSms()为静态同步方法,且main方法中只new一个对象,
 * 问是先打印"sendSms"还是先打印"call"?
 *
 * 答案:先打印"call",因为sendSms()方法锁的是Class模板,而call()方法锁的是调用者,二者锁的不是同一个东西
 * 所以先打印"call"!
 *
 * 问题8:同样,call()为非静态的同步方法,sendSms()为静态同步方法,main方法中new两个对象,分别调用
 * call()方法和sendSms()方法,问先打印什么?
 *
 * 答案:先打印call",理由同问题7的理由!
 */
//问题7的Test类
public class Test4 {

    public static void main(String[] args) {
        //new一个对象
        Phone4 phone = new Phone4();

        new Thread(()->{
            phone.sendSms();
        },"A").start();

        //捕获
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            phone.call();
        },"B").start();
    }
}
//问题8的Test类
public class Test4 {

    public static void main(String[] args) {
        //new一个/两个对象
        Phone4 phone1 = new Phone4();
        Phone4 phone2 = new Phone4();

        new Thread(()->{
            phone1.sendSms();
        },"A").start();

        //捕获
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            phone2.call();
        },"B").start();
    }
}

//Phone3只有唯一的一个Class对象
//即:Class<Phone3> phone3Class = Phone3.class; 全局唯一
class Phone4{

    //静态的同步方法:锁的是Class模板
    public static synchronized void sendSms(){
        //捕获
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sendSms");
    }

    //非静态的同步方法:锁的是调用者
    public synchronized void call(){
        System.out.println("call");
    }
}

6.集合类不安全

报:ConcurrentModificatioonException异常

List不安全

解决方案:

1.用Vector替换

2.使用

List<Object> synchronizedList = Collections.synchronizedList(new ArrayList<>());

3.使用并发容器,比如:CopyOnWriteArrayList

测试代码:

public class ListTest {
    public static void main(String[] args) {
        /**
         * 并发下ArrayList不安全
         *
         * 解决方案:
         * 1.List<String> list = new Vector<>();
         * 2.List<String> list = Collections.synchronizedList(new ArrayList<>());
         * 3.CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
         */

        /**
         * CopyOnWrite:写入时复制,是一种计算机设计领域的优化策略
         * 多个线程调用的时候,若是读取,则不操作原有list,而若是写入,则将原本集合中的内容复制一份,
         * 写入数据之后,再覆盖原本的内容!
         * 从而避免在写入的时候造成数据问题!也就是读写分离的思想!
         *
         * CopyOnWriteArrayList比Vector厉害在哪里?
         * 答案:只要有synchronized,效率就比较低,而Vector就是采用的synchronized。
         * 而CopyOnWriteArrayList采用的写入时复制的方法,其使用的是Lock锁,效率比Vector高!
         */
        CopyOnWriteArrayList<String> synchronizedList = new CopyOnWriteArrayList<>();

        for (int i=0; i<10; i++){
            new Thread(()->{
                synchronizedList.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(synchronizedList);
                synchronizedList.remove(0);
            },String.valueOf(i)).start();

        }
    }
}
Set不安全

解决方案:

1.使用Collections.synchronizedSet()方法解决

Set<String> set = Collections.synchronizedSet(new HashSet<>());

2.使用并发容器

CopyOnWriteArraySet<Object> set1 = new CopyOnWriteArraySet<>();
顺便一问:HashSet的底层是什么样的?

HashSet的底层就等于new一个HashMap:

public HashSet() {
    map = new HashMap<>();
}

//add
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}
HashMap不安全
回顾HashMap基础:
//1.map是这样用的吗? 答:不是,工作中不用HashMap
//2.默认等价于什么? 答:加载因子0.75、默认初始化容量1 << 4 = 16
Map<String, String> map = new HashMap<>();

解决方案:

Map<String, String> map =  new ConcurrentHashMap<>();

7.Callable

Callable与Runnable类似,不同点在于Callable:

1.可以有返回值

2.可以抛出异常

3.方法不同,Callable接口中是call()方法,而Runnable中是run()方法

启动线程的方式只有:new Thread();

而Thread()的参数又只接受Runnable引用类型的参数,不能接受Callable引用类型的参数,所以得通过将Callable来获得Runnable引用类型的参数,在将其交给Thread即可启动对应Callable的线程!FutureTask实现了Runnable接口,所以该类可以作为Runnable引用类型的参数,而正好FutureTask的也存在接受Callable接口的构造方法!所以可以这样调用:

new Thread(new FutureTask<V>(Callable callable)).start();
Runnable的实现类:FutureTask

FutureTask有两个构造方法,分别可以接受Runnable接口类型的参数和Callable接口类型的参数!

//测试demo:
public class CallableTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyThread myThread =  new MyThread();
        FutureTask futureTask = new FutureTask(myThread);
        new Thread(futureTask,"A").start();
        
        new Thread(futureTask,"B").start();//此处加一条代码,返回结果仍只为:call() 1024,因为结果会被缓存,提高效率

        //获取返回结果
		//这个get方法可能会产生阻塞,因为它要等待结果返回。所以一般把它放在最后或者采用异步通信来处理!
        Integer i = (Integer) futureTask.get();
        System.out.println(i);
    }
}

class MyThread implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        System.out.println("call()");
        return 1024;
    }
}
//返回结果:
call()
1024
细节:

1.有缓存

2.结果可能需要等待,会阻塞!

8.常用的辅助类(必会)

8.1、CountDownLatch -> 减法计数器

允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助!

CountDownLatch用给定的计数器初始化。await方法阻塞,直到由于countDown()方法的调用而导致当前计数达到零,之后所有等待线程被释放,并且任何后续的await调用立即返回!

//CountDownLatch是一个减法计数器
public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        //总数是6
        CountDownLatch countDownLatch = new CountDownLatch(6);

        for(int i=1; i<=6; i++){
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"Go Out!");
                countDownLatch.countDown();
            },String.valueOf(i)).start();
        }

        //等待计数器归零,然后再向下执行
        countDownLatch.await();

        System.out.println("6个学生都离开了教室,关门!");
    }
}
//输出打印为:
5Go Out!
1Go Out!
4Go Out!
2Go Out!
3Go Out!
6Go Out!
6个学生都离开了教室,关门!
    
//如果注释掉上方的countDownLatch.await();这行代码的话,则打印输出为:为何会先打印“6个学生都离开了教室,关门!”呢?这是因为
//线程开启需要一定的时间,如果没有countDownLatch.await();这行代码拦截的话,第一个线程都还没开启呢,main线程已经执行输出了:“6个学生都离开了教室,关门!”
6个学生都离开了教室,关门!
1Go Out!
2Go Out!
4Go Out!
6Go Out!
3Go Out!
5Go Out!
//这样就出现了错误,人都没走了,门就关了
原理:

主要有连个方法:

1.countDownLatch.countDown(); // 数量-1

2.countDownLatch.await(); //等待计数器归零,然后再向下执行

每次有线程调用countDownLatch.countDown()方法,就会数量-1,假设计数器变为0了,await()方法就会被唤醒,继续执行!

8.2、CyclicBarrier -> 加法计数器

允许一组线程全部等待彼此达到共同屏障点的同步辅助!

面试会问的:

下方测试代码中注意用lambda定义的匿名内部类:内部类lambad定义体中能拿到上方的变量i吗?答:拿不到,要拿也得通过final来拿,即new一个final的中间变量来拿,如下方的final int temp = i; (为什么拿不到局部变量i呢?深层原因就是局部变量存在于栈,而new Thread存在于堆中),其实java1.8之后不用final修饰temp也行,直接int temp = i;也可,因为你不写的话,系统会默认加一个final。

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        /**
         * 集齐7颗龙珠召唤神龙
         */

        //CyclicBarrier(int, Runnable)
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
            System.out.println("召唤神龙成功!");
        });

        for(int i=1; i<=7; i++){
            //下方lambad拿不到上方的变量i吗?答:拿不到,要拿也得通过final来拿,即new一个final的中间变量
            //来拿,如下方的final int temp = i; (为什么拿不到局部变量i呢?深层原因就是局部变量存在于栈,
            // 而new Thread存在于堆中),其实java1.8之后不用final修饰temp也行,直接int temp = i;也可,因为你
            //不写的话,系统会默认加一个final。被final修饰的常量存放于JVM方法区的常量池中。从而将栈区中的局部变量同步到方法区的
            //常量池中了,所以现在lambad定义的内部类可以拿到i了
            final int temp = i;
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"收集第"+temp+"个龙珠!");
                try {
                    //执行完cyclicBarrier.await();后该线程会被阻塞
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }

                //检验执行完cyclicBarrier.await();后该线程是否有被阻塞
                System.out.println("====");
            },String.valueOf(i)).start();
        }
    }
}
//打印输出:
5收集第5个龙珠!
2收集第2个龙珠!
3收集第3个龙珠!
1收集第1个龙珠!
4收集第4个龙珠!
6收集第6个龙珠!
7收集第7个龙珠!
召唤神龙成功!
====
====
====
====
====
====
====

//上述输出结果验证了:执行完cyclicBarrier.await();后该线程会被阻塞,因为最后才输出打印
====
====
====
====
====
====
====
8.3、Semaphore

Semaphore:信号量

Semaphore在限流的时候可以用!

public class SemaphoreDemo {
    public static void main(String[] args) {
        /**
         * 此处使用停车位的概念来打比方:比如有6辆车,但只有3个停车位,则最开始123停入,456得等待,
         * 之后3走了,变成124,2走了,变成154,1走了,变成654
         */
        //Semaphore(int permits),int permits可以理解为线程数量
        //线程数量:停车位
        Semaphore semaphore = new Semaphore(3);

        for(int i=1; i<=6; i++){
            new Thread(()->{
                // acquire() 获得
                //得到一个停车位
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+"抢到车位!");
                    //模拟停车时间
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println(Thread.currentThread().getName()+"离开车位!");

                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // release() 释放
                    //释放停车位
                    semaphore.release();
                }

            },String.valueOf(i
            )).start();
        }
    }
}

//打印输出结果为:
1抢到车位!
2抢到车位!
3抢到车位!
1离开车位!
4抢到车位!
3离开车位!
2离开车位!
5抢到车位!
6抢到车位!
4离开车位!
6离开车位!
5离开车位!
原理:

semaphore.acquire(); 获得,假设如果已经满了,则等待被释放!

semaphore.release(); 释放,会将当前的信号量释放,然后唤醒等待的线程!

作用:

多个共享资源互斥的使用!

并发限流,控制最大的线程数量!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值