高并发学习 —— JUC

Posted Johnny*

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了高并发学习 —— JUC相关的知识,希望对你有一定的参考价值。

Synchronized

并发编程的目的是为了提高程序的执行效率,提高程序的运行速度。但是并发编程功能还存在着一个致命问题:不同线程访问共享变量的同步问题。

Java的Synchronized关键字用于解决多个 线程之间访问资源的同步性。它保证任意时刻只能有一个线程访问synchronized修饰的方法或代码块。

Java官方从Java6开始在JVM层面对synchronized进行了优化。对锁的实现引入大量的优化:自旋锁、适应性自旋锁、锁消除、锁粗化、轻量级锁等技术来减少锁操作的开销。

synchronized的三种使用方式

  1. synchronized修饰实例方法。给对象实例加锁,进入同步代码前必须获得 当前对象实例的锁。
  2. synchronized修饰静态方法。 给当前类加锁,进入同步代码前要获得当前class的锁。
  3. synchronized修饰代码块。可以 指定加锁的对象(可以是对象也可以 是类)。

synchronized 修饰成员方法

package LockRange;

/**

synchronized是对象锁,锁的是实例。因此如果使用了synchronized,那么其他成员方法也不能同时调用

 */
public class Test1 {

    public static void main(String[] args) throws InterruptedException {
        Person person = new Person();
        new Thread(()->{ person.eat();}).start();
        Thread.sleep(1000);
        new Thread(()->{ person.say();}).start();
    }
}

class Person{
    public synchronized void say(){
        System.out.println("say something");
    }
    public synchronized void eat() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("eating");
    }
}

执行结果:

eating
say something

synchronized是对象锁。对于同一个实例(对象),对应唯一一把锁,每次只能被一个成员方法持有。其他未持有该锁的成员方法必须等待。
同时也要注意,synchronized的位置。如果是锁代码块,锁的是不同对象的话,那么是不同的两把锁,之间是 没有关系的。

synchronized 修饰静态方法、代码块

package LockRange;

public class TestStatic {

    public static void main(String[] args) throws InterruptedException {
        Person2 person = new Person2();
        new Thread(()->{ person.eat();}).start();
        Thread.sleep(1000);
        new Thread(()->{ person.say();}).start();
    }
}

class Person2{
    public static void say(){
    	//修饰静态方法
        synchronized(Person2.class){
            System.out.println("say something");
        }
    }
    //修饰代码块
    public  void eat() {
        synchronized(this){
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("eating");
        }

    }
}

执行结果:

say something
eating

say静态方法锁的是类,而eat方法锁得是实例,是类对象,两者是不同的。所以互不干扰。

synchronized关键字底层实现原理

synchronized同步代码块的实现使用的是:JVM的monitorenter和monitorexit指令,其中monitorenter指令指向同步代码开始的位置,monitorexit指令指向同步代码结束的位置。

在执行monitorenter指令时,线程视图获取锁也就是 对象监视器 monitor的持有权。如果锁的计数器为0则表示锁可以被获取,获取以后将计数器设为1也就是加1。
在执行monitorexit指令后,将锁计数器设为0,表明锁 被释放。如果获取对象锁失败,那当前线程就要阻塞等待,知道锁被另外一个线程释放才能再次尝试获取锁。

synchronized修饰的方法使用的ACC_SYNCHRONIZED 表示,该标识表明该 方法是一个同步方法,从而执行相应的同步调用。

售票例子

package synchronizedDemo;

class Ticket {
    private int tickets = 30;
    public  void sale(){
        synchronized(this){
            if( tickets > 0 ) {
                tickets--;
                System.out.println(Thread.currentThread().getName()+"卖出了1张票,剩余:"+tickets);
            }
        }


    }
}

public class SaleTicket{
    public static void main(String[] args) {
            Ticket ticket = new Ticket();
            new Thread(()->{ for(int i = 0; i < 30; i ++) ticket.sale(); }, "B").start();
            new Thread(()->{ for(int i = 0; i < 30; i ++) ticket.sale(); }, "A").start();
            new Thread(()->{ for(int i = 0; i < 30; i ++) ticket.sale(); }, "C").start();
    }
}

执行结果: A、B、C可能交替买票,是顺序打印的,因为synchronized的粒度比较粗,会打印完才释放掉锁,直至票数为0

B卖出了1张票,剩余:29
B卖出了1张票,剩余:28
B卖出了1张票,剩余:27
B卖出了1张票,剩余:26
B卖出了1张票,剩余:25
B卖出了1张票,剩余:24
B卖出了1张票,剩余:23
B卖出了1张票,剩余:22
B卖出了1张票,剩余:21
B卖出了1张票,剩余:20
B卖出了1张票,剩余:19
B卖出了1张票,剩余:18
B卖出了1张票,剩余:17
B卖出了1张票,剩余:16
B卖出了1张票,剩余:15
B卖出了1张票,剩余:14
B卖出了1张票,剩余:13
B卖出了1张票,剩余:12
B卖出了1张票,剩余:11
B卖出了1张票,剩余:10
B卖出了1张票,剩余:9
B卖出了1张票,剩余:8
B卖出了1张票,剩余:7
B卖出了1张票,剩余:6
B卖出了1张票,剩余:5
B卖出了1张票,剩余:4
B卖出了1张票,剩余:3
B卖出了1张票,剩余:2
B卖出了1张票,剩余:1
B卖出了1张票,剩余:0

Process finished with exit code 0

Java Guide 多线程

JUC

在这里插入图片描述

java.util.concurrent 并发包
java.util.concurrent.atomic 并发原子包
java.util.concurrent.locks 并发锁包,该包下有Lock、ReadWriteLock、Condition。
Condition将Object的监视方法(wait、notify、notifyAll)分解成不同的对象,将他们与Lock的任意实现相结合使用。

Lock 接口

【实现类】
Lock接口实现类有:
ReentrantLock(可重入锁)
ReentrantReadWriteLock.ReadLock(读锁)
ReentrantReadWriteLock.WriteLock(写锁)

【基本方法】

 Lock l = ...;
 l.lock();  	//1、获得锁
 
 // 2、 逻辑业务代码
 try {
   // access the resource protected by this lock
 } finally {
  //3、释放锁
   l.unlock();
 }

【常用方法】
在这里插入图片描述
ReentrantLock的构造方法
在这里插入图片描述

售票例子

package lockDemo;


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

class Ticket2 {
    private int tickets = 30;
    public  void sale(){
        Lock lock = new ReentrantLock();
        lock.lock(); //1、获取锁
        //2、逻辑业务代码
        try{

            if( tickets > 0 ) {
                tickets--;
                System.out.println(Thread.currentThread().getName()+"卖出了1张票,剩余:"+tickets);
            }

        }finally {
            lock.unlock(); //3、 释放掉锁
        }


    }
}

public class SaleTicket2{
    public static void main(String[] args) {
            Ticket2 ticket = new Ticket2();
            new Thread(()->{ for(int i = 0; i < 30; i ++) ticket.sale(); }, "B").start();
            new Thread(()->{ for(int i = 0; i < 30; i ++) ticket.sale(); }, "A").start();
            new Thread(()->{ for(int i = 0; i < 30; i ++) ticket.sale(); }, "C").start();
    }
}

打印顺序是错乱的,但是票销售为 0时停止:

B卖出了1张票,剩余:29
C卖出了1张票,剩余:27
B卖出了1张票,剩余:26
C卖出了1张票,剩余:25
A卖出了1张票,剩余:28
C卖出了1张票,剩余:23
B卖出了1张票,剩余:24
C卖出了1张票,剩余:21
A卖出了1张票,剩余:22
C卖出了1张票,剩余:19
B卖出了1张票,剩余:20
C卖出了1张票,剩余:17
A卖出了1张票,剩余:18
C卖出了1张票,剩余:15
B卖出了1张票,剩余:16
C卖出了1张票,剩余:13
A卖出了1张票,剩余:14
C卖出了1张票,剩余:11
B卖出了1张票,剩余:12
C卖出了1张票,剩余:9
A卖出了1张票,剩余:10
C卖出了1张票,剩余:7
B卖出了1张票,剩余:8
C卖出了1张票,剩余:5
A卖出了1张票,剩余:6
C卖出了1张票,剩余:3
B卖出了1张票,剩余:4
C卖出了1张票,剩余:1
A卖出了1张票,剩余:2
B卖出了1张票,剩余:0

synchronize和Lock 的区别

  1. synchronized是Java中内置的的关键字,Lock是接口。
  2. synchronized不可以获得锁的状态,Lock可以通过tryLock判断锁的状态。
  3. synchronized会自动释放掉锁,而Lock需要手动在finally处释放,否则会发生死锁。
  4. synchronized如果获取不到锁会一直等待下去,而Lock可以中断等待。
  5. synchronized是可重入、不可中断的非公平锁,而Lock是可重入锁,可以通过构造方法来决定是公平锁还是非公平锁。
  6. synchronized适合锁少量的代码同步问题,Lock适合锁大量同步代码!

什么是可重入锁?
可重入锁是指自己可以以不同的方式再次访问临界资源而不出现死锁等相关问题。经典之处在于判断了需要使用锁的线程是否为加锁的线程。如果是,则拥有重入的能力。可重入锁在设计上不仅判断锁有没有被锁上,还会判断锁是谁锁上的,当就是自己锁上的时候,那么他依旧可以再次访问临界资源,并把加锁次数加1。只有等待锁计数器降为0是才能释放锁。

什么是公平锁?
所谓公平锁就是 就是遵循先来后到的原则。而非公平锁允许插队。Lock默认构造方法是非公平锁,原因是 为了提高CPU利用率,保证公平。比如有两个线程A、B先后到达,A执行时间是30min,B是2秒,那么CPU根据非公平锁的调度策略会 先执行B的。

线程之间的通信问题

单例模式、排序算法、生产者消费者问题

生产者 消费者问题

在这里插入图片描述

package ThreadCommunication;

public class Communication {
    public static void main(String[] args) {

        Buffer buffer = new Buffer();
        Producer p = new Producer(buffer);
        Consumer c = new Consumer(buffer);


        new Thread(p, "A").start();
        new Thread(c, "B").start();
        new Thread(p, "C").start();
        new Thread(c, "D").start();

    }

}
class Buffer {

    //资源
    private int num = 0;
    //信号量 true表示槽池中为空  false表示槽池不为空 初始状态为空
    private boolean empty = true;

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }

    public synchronized void consume() throws InterruptedException {

        //1、等待判断
        while ( empty){//只要槽池为空就阻塞等待
            this.wait();
        }
        //2、业务操作
        this.num = this.num - 1;
        System.out.println("线程"+Thread.currentThread().getName()+" - >"+this.getNum());
        //3、信号量置为1 同时通知其他线程
        this.empty = true;
        this.notifyAll();

    }

    //线程同步 记得要加同步锁
    public synchronized void produce() throws InterruptedException {

        //1、等待判断(注意这里是while不能是if)
        while ( !empty){//槽池满了就等待
            this.wait();
        }
        //2、业务操作
        this.num = this.num + 1;
        System.out.println("线程"+Thread.currentThread().getName()+" - >"+this.getNum());

        //3、信号量置为false 同时通知其他线程
        this.empty = false;
        this.notifyAll();
    }
}
class Producer implements Runnable{
    private Buffer buffer;
    public Producer(Buffer buffer){
        this.buffer = buffer;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
                buffer.produce();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

class Consumer implements Runnable{

    private Buffer buffer;
    public Consumer(Buffer buffer){
        this.buffer = buffer;
    }
    @Override
    public void run() {

        while( true ){
            try {
                Thread.sleep(1000);
                buffer.consume();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

注意 consume()和produce的判断等待方法中使用 的是while而不是if,因为if会有虚假唤醒问题。原因如下。

if改成while 循环监听。
假设有四个线程,A、C是Producer,B、D是Consumer。
当number是1时,C抢到了produce的锁。if语句判断后 ,进入wait状态,而wait会放弃掉锁,也就意味着当再次被唤醒时要重新跟其他线程抢锁,如果抢不到,该线程就会阻塞。假设C放弃的同时A抢到了produce的锁,由判断条件和A一起处于wait状态。此时方法外只有B、D了。假设B抢到锁进行消费操作之后number被置为0,同时notifyAll唤醒其他线程。由于系统调度会安排C、A(因为等待时间越长优先级越高)执行。此时C(A)还进行在if语句内,如果 这个时候没有将进行循环判断,则C会对number进行加1,之后的A线程也会进行加1使得number变成2;

在这里插入图片描述

线程如果被唤醒后再次执行时,并不是重新进入方法,而是从上次阻塞的位置从下开始运行,也就是从wait()方法后开始执行。所以判断是否进入某一线程的条件 是用while判断,而不是用If判断判断。

验证如下:

package ThreadCommunication;

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

public class C {

    
    public static void main(String[] args) {
       Data3 d = new Data3();


       new Thread(()->{ for (int i = 0; i < 2; i++) d.printB(); }, "B").start();
       new Thread(()->{ for (int i = 0; i < 2; i++) d.printA(); }, "A").start();

    }
}

//资源类
class Data3{
    private int num = 1;
    Lock lock = new ReentrantLock();
    //1、获得监视器
    Condition conditionA = lock.newCondition();
    Condition conditionB = lock.newCondition();

    public void printB(){

        System.out.println("进入printB方法");
        lock.lock尚硅谷JUC高并发编程学习笔记JUC简介与Lock接口

尚硅谷JUC高并发编程学习笔记Callable,FutureTask,JUC辅助类

尚硅谷JUC高并发编程学习笔记Callable,FutureTask,JUC辅助类

尚硅谷JUC高并发编程学习笔记Callable,FutureTask,JUC辅助类

尚硅谷JUC高并发编程学习笔记

尚硅谷JUC高并发编程学习笔记