【JUC 一】线程 进程 synchronized Lock锁 生产者 消费者 8锁 线程安全集合类

1、什么是JUC?

java.util.concurrent

java.util.concurrent.atomic

java.util.concurrent.locks

JUC就是三个并发工具包的简称

业务: 普通的线程代码, 之前都是用的thread或者runnable接口

但是相比于callable来说,thread没有返回值,且效率没有callable高

2、线程和进程

  • 进程

    一个运行中的程序的集合;一个进程往往可以包含多个线程,至少包含一个线程

  • java默认有几个线程?

    两个。 main线程、gc线程

  • 线程

    线程(thread)是操作系统能够进行运算调度的最小单位

  • 对于java而言如何创建thread

    继承自thread

    实现runnable接口

    实现callable接口

  • Java真的可以开启线程吗?

    开不了的。底层是用native关键词修饰。调用本地实现(start0())

 public synchronized void start() {
     /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
     if (threadStatus != 0)
         throw new IllegalThreadStateException();

     /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
     group.add(this);

     boolean started = false;
     try {
         start0();
         started = true;
     } finally {
         try {
             if (!started) {
                 group.threadStartFailed(this);
             }
         } catch (Throwable ignore) {
             /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
         }
     }
 }
//本地方法,调用底层c++, java无法操作硬件
private native void start0();

并发,并行

并发编程: 并发和并行

并发(多线程操作同一个资源,交替执行)

  • CPU一核, 模拟出来多条线程,天下武功,唯快不破,快速交替

并行(多个人一起行走, 同时进行)

  • CPU多核,多个线程同时进行 ; 使用线程池操作
public static void main(String[] args) {
    //获取CPU核数
    //CPU 密集型,IO密集型
    System.out.println(Runtime.getRuntime().availableProcessors());
}

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

  • 线程有几个状态?
public enum State {
    // 新生
    NEW,

    // 运行
    RUNNABLE,

    // 阻塞
    BLOCKED,

    // 等待
    WAITING,

    //超时等待
    TIMED_WAITING,

    //终止
    TERMINATED;
}

区别 wait sleep
来自不同的类 object类 线程类
锁的释放不同 会释放锁 不会释放锁
使用范围不同 必须在同步代码中 可以在任何地方睡

TimeUnit

TimeUnit.SECONDS.sleep(3);//睡眠3秒

3、Lock锁(重点)

3.1、传统synchronized

本质: 队列和锁,放在方法上锁的是this,放在代码块中锁的是()里面的对象

synchronized(obj){    
}

3.2、Lock 接口

  • reentrantLock构造器

非公平锁:NonfairSync()

十分不公平,可以插队(默认)

公平锁:FairSync()

十分公平,先来后到,一定要排队

public ReentrantLock() {
    sync = new NonfairSync(); //无参默认非公平锁
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();//传参为true为公平锁
}

3.3、synchronized 和 lock 区别

比较 Lock synchronized
类型 java接口 内置关键字
开启释放 显式锁
手动 开启 和 关闭
隐式锁
出了作用域自动释放
代码锁 支持 支持
方法锁 不支持 支持
锁状态 可以判断是否获取到了锁 无法判断获取锁的状态
线程阻塞 Lock锁就不一定会等待下去 线程1(获得锁,阻塞),线程2(等待)
可重入性 可重入的,可以判断锁 可重入锁,不可以中断的
公平性 默认非公平的(可设置) 非公平的
性能 性能更好
JVM将花费较少的时间来调度
性能一般
JVM将花费较多的时间来调度管理
扩展性 有更好的扩展性(提供更多的子类) 不支持
适用场景 适合锁大量的同步代码 适合锁少量的代码同步问题

4、生产者和消费者问题

面试高频:单例模式、八大排序、生产者消费者、死锁

4.1、synchronized实现

wait、notify 必须在 synchronizied 声明的代码中,否则会抛出异常 java.lang.IllegalMonitorStateException

package providerConsumer;

/**
 * @author ajun
 * Date 2021/7/3
 * @version 1.0
 * synchronized版生产者消费者
 */
public class Syn {
    public static void main(String[] args) {
        Data data = new Data();

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

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

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

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

    }
}

class Data{
    private int num;

    public int getNum() {
        return num;
    }

    //增加
    public synchronized void increment() throws InterruptedException {
        //判断等待
        //用while,不用if,防止虚假唤醒
        while(num!=0){
            this.wait();
        }
        //业务代码
        num++;
        System.out.println(Thread.currentThread().getName() + "  -->  " + num);
        //通知
        this.notifyAll();
    }

    //减少
    public synchronized void decrement() throws InterruptedException {
        //判断等待
        //用while,不用if,防止虚假唤醒
        while(num == 0){
            this.wait();
        }
        //业务代码
        num--;
        System.out.println(Thread.currentThread().getName() + "  -->  " + num);
        //通知
        this.notifyAll();
    }
}
  • 可能存在的问题

虚假唤醒

在判断等待时,如果用 if ,当线程多的时候,可能会有虚假唤醒

解决办法

if 判断改为 while 判断

因为 if 只会执行一次,执行完会接着向下执行 if()外边的

而 while 不会,直到条件满足才会向下执行 while()外边的

4.2、JUC实现

在JUC中,Lock 替换 synchronized,await 替换 wait,signal 替换 notify。(signal 信号)


package providerConsumer;

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

/**
 * @author ajun
 * Date 2021/7/3
 * @version 1.0
 */
public class Loc {
    public static void main(String[] args) {
        Data2 data2 = new Data2();
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    data2.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

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

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

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

class Data2{
    private int num = 0;

    Lock lock = new ReentrantLock();//定义锁
    Condition condition = lock.newCondition();//定义同步监视器

    public int getNum() {
        return num;
    }

    //增加
    public void increment() throws InterruptedException {
        lock.lock();//加锁
        try {
            //判断等待
            while (num != 0){
                condition.await();//等待
            }
            //业务代码
            num++;
            System.out.println(Thread.currentThread().getName() + " -->  " + num);
            //通知
            condition.signalAll();
        } finally {
            lock.unlock();//解锁
        }
    }

    //减少
    public void decrement() throws InterruptedException {
        lock.lock();//加锁
        try {
            //判断等待
            while (num == 0){
                condition.await();//等待
            }
            //业务代码
            num--;
            System.out.println(Thread.currentThread().getName() + "  -->  " + num);
            //通知
            condition.signalAll();
        } finally {
            lock.unlock();//解锁
        }
    }
}

Condition实现精准通知唤醒

5、8锁现象

8锁就是关于锁的八个现象

① 非静态同步方法的默认锁是 this,静态同步方法的默认锁是 class
② 某一时刻内,只能有一个线程有锁,无论几个方法;前提是用的同一把锁

参考:https://www.cnblogs.com/shamao/p/11045282.html

5.1、2线程 1对象 2方法

两个线程 调用 同一个对象 的 两个同步方法

package lock8;

/**
 * @author ajun
 * Date 2021/7/4
 * @version 1.0
 * 两个线程调用同一个对象的两个同步方法
 */
public class Lock1 {
    public static void main(String[] args) {
        //同一对象
        Number number = new Number();

        //线程1
        new Thread(() -> {number.getOne();},"A").start();
        
        //线程2
        new Thread(() -> {number.getTwo();},"B").start();

    }
}

class Number{
    //同步方法1 (非static)
    public synchronized void getOne(){
        System.out.println(Thread.currentThread().getName() + ": one");
    }
    //同步方法2 (非static)
    public synchronized void getTwo(){
        System.out.println(Thread.currentThread().getName() + ": two");
    }
}

运行结果如下:

A: one
B: two

分析:

被 synchronized 修饰的方法,锁的对象是方法的调用者。因为两个方法的调用者是同一个,所以两个方法用的是同一个锁,先调用方法的先获得锁,先执行

5.2、2线程 1对象 2方法(1sleep)

新增sleep()给某个方法

TimeUnit.SECONDS.sleep(2);

分析:

不管在何处添加休眠后,会中途休眠,但不影响执行顺序。

被synchronized修饰的方法,锁的对象是方法的调用者。因为两个方法的调用者是同一个,所以两个方法用的是同一个锁,先调用方法的先获得锁,先执行,第二个方法只有在第一个方法执行完释放锁之后才执行

5.3、3线程 1对象 3方法(1普通)

新增一个线程调用 同一对象 新增的一个普通方法

分析:

新增的方法没有被 synchronized 修饰,不是同步方法,不受锁的影响,所以不需要等待。其他线程共用了一把锁,所以还需要等待。

5.4、2线程 2对象 2方法(1sleep)

两个线程 调用 两个对象 的同步方法,其中一个方法有sleep()

分析:

被 synchronized 修饰的方法,锁的对象是方法的调用者。因为用了两个对象调用各自的方法,所以两个方法的调用者不是同一个,所以两个方法用的不是同一个锁,后调用的方法不需要等待先调用的方法

5.5、2线程 1对象 2方法(1static)

sleep()的方法设置为static,并且让两个线程用同一个对象调用两个方法

分析:

被synchronized和static修饰的方法,锁的对象是类的class对象。仅仅被synchronized修饰的方法,锁的对象是方法的调用者。因为两个方法锁的对象不是同一个,所以两个方法用的不是同一个锁,后调用的方法不需要等待先调用的方法。

5.6、2线程 1对象 2方法(2sattic)

将两个方法均设置为static方法,并且让两个线程用同一个对象调用两个方法

分析:

被synchronized和static修饰的方法,锁的对象是类的class对象。因为两个同步方法都被static修饰了,所以两个方法用的是同一个锁,后调用的方法需要等待先调用的方法

5.7、2线程 2对象 2方法(1static)

将两个方法中有sleep()的方法设置为static方法,另一个方法去掉static修饰,让两个线程用 两个对象 调用两个方法

分析:

被synchronized和static修饰的方法,锁的对象是类的class对象。仅仅被synchronized修饰的方法,锁的对象是方法的调用者。即便是用同一个对象调用两个方法,锁的对象也不是同一个,所以两个方法用的不是同一个锁,后调用的方法不需要等待先调用的方法。

5.8、2线程 2对象 2方法(2static)

将两个方法均设置为static方法,并且让两个线程用 一个对象 调用两个方法

分析:

被synchronized和static修饰的方法,锁的对象是类的class对象。因为两个同步方法都被static修饰了,即便用了两个不同的对象调用方法,两个方法用的还是同一个锁,后调用的方法需要等待先调用的方法。

5.9、总结

  • 普通同步方法

    • 一个类里面如果有多个synchronized方法,在使用同一个对象调用的前提下,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,其他的线程都只能等待,换句话说,某一时刻内,只能有唯一一个线程去访问这些synchronized方法
    • 锁的是当前对象this,被锁定后,其他线程都不能进入到当前对象的其他的synchronized方法。
  • 普通方法

    • 加个普通方法后发现和同步锁无关
  • 静态同步方法

    • 换成静态同步方法后,情况又变化
    • 所有的非静态同步方法用的都是同一把锁:实例对象本身
    • 也就是说如果一个对象的非静态同步方法获取锁后,该对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,可是其他对象的非静态同步方法因为跟该对象的非静态同步方法用的是不同的锁,所以毋须等待该对象的非静态同步方法释放锁就可以获取他们自己的锁
    • 所有的静态同步方法用的也是同一把锁:类对象本身
    • 这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间不会有竞争条件。但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,而不管是同一个对象的静态同步方法,还是其他对象的静态同步方法,只要它们属于同一个类的对象,那么就需要等待当前正在执行的静态同步方法释放锁

6、线程安全集合类

6.1、CopyOnWriteArrayList

并发下 ArrayList 是不安全的

  • 解决方案

    1、用线程安全的 Vector 替换 ArrayList

    底层是用 synchronized ,效率不高

    2、用集合工具类把 ArrayList 转换为线程安全的

    底层是用 synchronized ,效率不高

    Collections.synchronizedList(new ArrayList<>())

    3、使用 CopyOnWriteArrayList 替换 ArrayList

    写入时复制,COW 是计算机程序设计领域的一种优化策略

    底层是用 Lock 锁,效率比前两种方式高。读写分离。


package jUCCollection;

import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * @author ajun
 * Date 2021/7/4
 * @version 1.0
 */
public class CopyOnWriteArrayListTest {
    public static void main(String[] args) {
        //List<String> list = new ArrayList<>();
        //List<String> list = new Vector<>();
        //List<String> list = Collections.synchronizedList(new ArrayList<>());
        List<String> list = new CopyOnWriteArrayList<>();
        
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(Thread.currentThread().getName() + " : "+list);
            },String.valueOf(i)).start();
        }
    }
}

6.2、CopyOnWriteArraySet

并发下 HashSet 是不安全的

  • 解决方案

    1、用集合工具类把 HashSet 转换为线程安全的

    底层是用 synchronized ,效率不高

    Collections.synchronizedSet(new HashSet<>())

    2、使用 CopyOnWriteArraySet 替换 HashSet

    写入时复制,COW 计算机程序设计领域的一种优化策略

    写入时调用 CopyOnWriteArrayList 的方法,和 CopyOnWriteArrayList 基本一致,略有区别

    底层是用 Lock 锁,效率比前一种方式高。读写分离。

6.3、ConcurrentHashMap

并发下 HashMap 是不安全的

  • 解决方案

    1、用集合工具类把 HashMap 转换为线程安全的

    效率不高

    Collections.synchronizedMap(new HashMap<>())

    2、使用 ConcurrentHashMap 替换 HashMap

    效率比前一种方式高

posted @ 2021-07-27 12:17  土味儿  阅读(48)  评论(0)    收藏  举报