Java 多线程及并发知识大集合

「这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战

并发

多线程

实现多线程的三种方式

Runnable

  1. class CountDown implements Runnable

启动方式:new Thread(new CountDown(),"Runnable").start();//new Thread()第二个参数是线程名字 方法定义:

public void run() {
}
复制代码

Callable

  1. class ThreadCall implements Callable<String>String是可以替换的,是回调的值

启动方式: 使用这个接口要使用FutrueTask类作为简单的适配类

FutureTask<String> ft = new FutureTask<>(new ThreadCall());
new Thread(ft,"Callable").start()
复制代码

方法定义:

@Override
public String call() throws Exception {
    return "";
}
复制代码

特别注意 (1)Callable规定的方法是call(),而Runnable规定的方法是run(). (2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值的,为了支持此功能,Java中提供了Callable接口。
(3)call()方法可抛出异常,而run()方法是不能抛出异常的。 (4)运行Callable任务可拿到一个Future对象, Future表示异步计算的结果。 (5)FutureTask 的get()方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回;

Thread

  1. class MyThread extends Thread

启动方式:

new MyThread().start();
复制代码

方法定义:

public void run() {
}
复制代码
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class SynTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        new Thread(new CountDown(),"Runnable").start();
        new MyThread().start();
        int time = 5;
        FutureTask<String> ft = new FutureTask<>(new ThreadCall());
        new Thread(ft,"Callable").start();
        String output=ft.get();
        System.out.println("Callable output is:"+output);
        System.out.println("以下是被堵塞的线程");
        while(time>=0){
            System.out.println(Thread.currentThread().getName() + ":" +time );
            time--;
            try {
                Thread.sleep(1000);                                                    //睡眠时间为1秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
class ThreadCall implements Callable<String> {
    @Override
    public String call() throws Exception {
        // TODO Auto-generated method stub
        int time=5;
        while (time>0) {
            System.out.println(Thread.currentThread().getName() + ":" + time--);
            try {
                Thread.sleep(1000);                                                    //睡眠时间为1秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return time+"";
    }
}



class MyThread extends Thread{
    int time=5;
    public void run() {
        Thread.currentThread().setName("Thread");
        while (time>=0){
            System.out.println(Thread.currentThread().getName() + ":" + time--);
            try {
                Thread.sleep(1000);                                                    //睡眠时间为1秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class CountDown implements Runnable{
    int time = 5;
    public void run() {
        while(time>=0){
            System.out.println(Thread.currentThread().getName() + ":" + time--);
            try {
                Thread.sleep(1000);                                                    //睡眠时间为1秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
复制代码

线程同步

竞争状态

当多个线程访问同一公共资源并引发冲突造成线程不同步,我们称这种状态为竞争状态。

线程安全

是针对类来说的,如果一个类的对象在多线程程序中不会导致竞争状态,我们就称这类为线程安全的,如StringBuffer是线程安全的,而StringBuilder则是线程不安全的。

临界区

造成竞争状态的原因是多个线程同时进入了程序中某一特定部分,我们称这部分为程序中的临界区,我们要解决多线程不同步问题,就是要解决如何让多个线程有序的访问临界区。

java在处理线程同步时常用方法

1、synchronized关键字。

2、Lock显示加锁。

3、信号量Semaphore。

volatile关键字

volatile关键字经常在并发编程中使用,其特性是保证可见性以及有序性

Java内存模型

所有的变量都存储在主内存中。每条线程中还有自己的工作内存,线程的工作内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来)。线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

image.png 基于此种内存模型,便产生了多线程编程中的数据“脏读”等问题。 例如两个线程分别读取10存入各自所在的工作内存当中,然后线程1进行加100操作,然后把i的最新值101写入到内存。此时线程2的工作内存当中i的值还是10,进行加1操作之后,i的值为11,然后线程2把i的值写入内存,为11,但是想要111。

并发编程的三大概念:原子性,有序性,可见性

要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

原子性

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。 Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性

可见性

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

//线程1执行的代码
int i = 0;
i = 10;
//线程2执行的代码
j = i;
复制代码

当线程1执行 i =10这句时,会先把i的初始值加载到工作内存中,然后赋值为10,那么在线程1的工作内存当中i的值变为10了,却没有立即写入到主存当中。 此时线程2执行 j = i,它会先去主存读取i的值并加载到线程2的工作内存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10. 这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。 Java提供了volatile关键字来保证可见性 当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。 通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

有序性 有序性:即程序执行的顺序按照代码的先后顺序执行 指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的 因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行

//线程1:
context = loadContext();   //语句1,加载配置环境
inited = true;             //语句2,加载配置环境成功

 //线程2:
while(!inited ){    //等待加载配置环境
   sleep()
}
doSomethingwithconfig(context); //用配置环境工作
复制代码

于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。

指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

volatile保证可见性

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义: 1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

//如果stop不用volatile修饰,那么可能性很小当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
//线程1
boolean stop = false;
while(!stop){
    doSomething();
}
//线程2
stop = true;
复制代码

第一:使用volatile关键字会强制将修改的值立即写入主存;

第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);

第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。

volatile不能确保原子性

Note:自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存三个操作步骤。

线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了

线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,也不会导致主存中的值刷新,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存

然后线程1接着进行加1操作,由于已经读取了inc的值,注意此时在线程1的工作内存中inc的值仍然为10(可以这样认为,把inc赋值给变量a,变量a又赋值给变量b,那么inc的值会重新刷新(inc==10为false!!!),但是b的值不会刷新了),所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存

那么两个线程分别进行了一次自增操作后,inc只增加了1

volatile保证有序性

volatile关键字禁止指令重排序有两层意思: volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。

1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

2)在进行指令优化时,不能将在对volatile变量的读操作或者写操作的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

synchronized关键字

不太清楚synchronized以及notify的用法,以及锁方法,锁类的实现原理,还有信号量,感觉都是不同的东西,但是具体的用法忘记的差不多了。 官方解释: Synchronized同步方法可以支持使用一种简单的策略来防止线程干扰和内存一致性错误:如果一个对象对多个线程可见,则对该对象变量的所有读取或写入都是通过同步方法完成的。

简单就是说Synchronized的作用就是Java中解决并发问题的一种最常用最简单的方法 ,他可以确保同一个时刻最多只有一个线程执行同步代码,从而保证多线程环境下并发安全的效果。 如果有一段代码被Synchronized所修饰,那么这段代码就会以原子的方式执行,当多个线程在执行这段代码的时候,它们是互斥的,不会相互干扰,不会同时执行。

Synchronized工作机制是在多线程环境中使用一把锁,在第一个线程去执行的时候去获取这把锁才能执行,一旦获取就独占这把锁直到执行完毕或者在一定条件下才会去释放这把锁,在这把锁释放之前其他的线程只能阻塞等待

synchronized是Java中的关键字,被Java原生支持,是一种最基本的同步锁。 它修饰的对象有以下几种:   1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象。   2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象。   3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象。   4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

线程不安全的例子

售票员问题:

可能会卖同一张票,没有票了可能也会继续卖
    public static void main(String[] args) {
	// write your code here
        MyThread1 r1=new MyThread1();
        Thread t1=new Thread(r1,"First_Thread1");
        Thread t2=new Thread(r1,"Second_Thread2");
        Thread t3=new Thread(r1,"Third_Thread2");
        Thread t4=new Thread(r1,"Forth_Thread2");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }

    static  class  MyThread1 implements Runnable{
        public  static  Integer num=10;//通过静态变量,让他们卖的是同一张票
        @Override
        public  void run() {
            while (num>0){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println( Thread.currentThread().getName()+ "  正在卖 "+num--+" 张车票");
            }
        }
    }



Forth_Thread2  正在卖 10 张车票
First_Thread1  正在卖 9 张车票
Third_Thread2  正在卖 8 张车票
Second_Thread2  正在卖 7 张车票
First_Thread1  正在卖 6 张车票
Forth_Thread2  正在卖 5 张车票
Second_Thread2  正在卖 6 张车票
Third_Thread2  正在卖 4 张车票
Second_Thread2  正在卖 3 张车票
Forth_Thread2  正在卖 1 张车票
First_Thread1  正在卖 2 张车票
Third_Thread2  正在卖 0 张车票
Second_Thread2  正在卖 -1 张车票
复制代码

synchronized 修饰方法

//如果synchronized是锁起来整个方法的,synchronized修饰函数不需要传入字符串参数,相当于默认是this

对于此案例,是两个线程之间竞争售票,因此不适宜锁起来整个方法

    static  class  MyThread1 implements Runnable{
        public  static  Integer num=10;
        @Override
        public synchronized  void run() {
            while (num>0){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println( Thread.currentThread().getName()+ "  正在卖 "+num--+" 张车票");
            }
        }
    }
只有First_Thread在卖票了
First_Thread1  正在卖 10 张车票
First_Thread1  正在卖 9 张车票
First_Thread1  正在卖 8 张车票
First_Thread1  正在卖 7 张车票
First_Thread1  正在卖 6 张车票
First_Thread1  正在卖 5 张车票
First_Thread1  正在卖 4 张车票
First_Thread1  正在卖 3 张车票
First_Thread1  正在卖 2 张车票
First_Thread1  正在卖 1 张车票
复制代码

synchronized()中锁的是Object对象

synchronized 修饰代码块

可以定义公共的str静态变量,若不定义为static静态,则两个线程的str是线程自己的,而不是公共的

也可以用synchronized (" ") { //在需要加锁保证完整运行的代码块旁边加上synchronized (" "){}包裹代码,即可锁起来该部分代码,()内的字符串随意定义

    static  class  MyThread1 implements Runnable{
        public  static  Integer num=10;
        public  static String str = new String("weimeig");
        @Override
        public void run() {
            while (true){
                synchronized (str){
                    if(num>0){
                        System.out.println( Thread.currentThread().getName()+ "  正在卖 "+num--+" 张车票");
                    }else {
                        break;
                    }
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }
    }


Forth_Thread2  正在卖 10 张车票
Second_Thread2  正在卖 9 张车票
First_Thread1  正在卖 8 张车票
Third_Thread2  正在卖 7 张车票
Third_Thread2  正在卖 6 张车票
Forth_Thread2  正在卖 5 张车票
First_Thread1  正在卖 4 张车票
Second_Thread2  正在卖 3 张车票
Third_Thread2  正在卖 2 张车票
First_Thread1  正在卖 1 张车票
复制代码

wait notify

  1. wait()使当前线程阻塞,前提是 必须先获得锁,一般配合synchronized 关键字使用,即,一般在synchronized 同步代码块里使用 wait()、notify/notifyAll() 方法。
  2. 由于 wait()、notify/notifyAll() 在synchronized 代码块执行,说明当前线程一定是获取了锁的。

当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。 只有当 notify/notifyAll() 被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized 代码块的代码或是中途遇到wait() ,再次释放锁。 也就是说,notify/notifyAll() 的执行只是唤醒沉睡的线程,而不会立即释放锁,锁的释放要看代码块的具体执行情况。所以在编程中,尽量在使用了notify/notifyAll() 后立即退出临界区,以唤醒其他线程让其获得锁 3. wait() 需要被try catch包围,以便发生异常中断也可以使wait等待的线程唤醒。 4. notify 和 notifyAll的区别 notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。notifyAll 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll 方法。比如在生产者-消费者里面的使用,每次都需要唤醒所有的消费者或是生产者,以判断程序是否可以继续往下执行。

import java.util.LinkedList;
import java.util.Queue;

public class ProducerAndConsumer {
    private final int MAX_LEN = 10;
    private Queue<Integer> queue = new LinkedList<Integer>();
    class Producer extends Thread {
        @Override
        public void run() {
            producer();
        }
        private void producer() {
            while(true) {
                synchronized (queue) {
                    while (queue.size() == MAX_LEN) {
                        queue.notify();
                        System.out.println("当前队列满");
                        try {
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    queue.add(1);
                    queue.notify();
                    System.out.println("生产者生产一条任务,当前队列长度为" + queue.size());
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    class Consumer extends Thread {
        @Override
        public void run() {
            consumer();
        }
        private void consumer() {
            while (true) {
                synchronized (queue) {
                    while (queue.size() == 0) {
                        queue.notify();
                        System.out.println("当前队列为空");
                        try {
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    queue.poll();
                    queue.notify();
                    System.out.println("消费者消费一条任务,当前队列长度为" + queue.size());
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    public static void main(String[] args) {
        ProducerAndConsumer pc = new ProducerAndConsumer();
        Producer producer = pc.new Producer();
        Consumer consumer = pc.new Consumer();
        producer.start();
        consumer.start();
    }
}

复制代码

Lock下的 Condition 的await和signal()

在java Lock体系下依然会有同样的方法实现等待/通知机制。从整体上来看Object的wait和notify/notify是与对象监视器配合完成线程间的等待/通知机制,而Condition与Lock配合完成等待通知机制,前者是java底层级别的,后者是语言级别的,具有更高的可控制性和扩展性

针对Object的notify/notifyAll方法 void signal():唤醒一个等待在condition上的线程,将该线程从等待队列中转移到同步队列中,如果在同步队列中能够竞争到Lock则可以从等待方法中返回。 void signalAll():与1的区别在于能够唤醒所有等待在condition上的线程

针对Object的wait方法 void await() throws InterruptedException:当前线程进入等待状态,如果其他线程调用condition的signal或者signalAll方法并且当前线程获取Lock从await方法返回,如果在等待状态中被中断会抛出被中断异常; long awaitNanos(long nanosTimeout):当前线程进入等待状态直到被通知,中断或者超时; boolean await(long time, TimeUnit unit)throws InterruptedException:同第二种,支持自定义时间单位 boolean awaitUntil(Date deadline) throws InterruptedException:当前线程进入等待状态直到被通知,中断或者到了某个时间

Condition优点

  1. Condition能够支持不响应中断,而通过使用Object方式不支持;
  2. Condition能够支持多个等待队列(new 多个Condition对象),而Object方式只能支持一个;
  3. Condition能够支持超时时间的设置,而Object不支持

Lock接口

lock 概念

  1. Lock是一个接口,类似于List
  2. Lock和ReadWriteLock接口是两大锁的根接口
  3. Lock代表实现类是ReentrantLock(可重入锁)
  4. ReadWriteLock(读写锁)的代表实现类是ReentrantReadWriteLock。
  5. Condition 接口描述了可能会与锁有关联的条件变量。这些变量在用法上与使用 Object.wait 访问的隐式监视器类似,但提供了更强大的功能。需要特别指出的是,单个 Lock 可能与多个 Condition 对象关联。为了避免兼容性问题,Condition 方法的名称与对应的 Object 版本中的不同。

锁的名词

1. 可重入锁

可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。前提得是同一个对象或者class) 锁的分配机制:基于线程的分配,而不是基于方法调用的分配。 可重入,就是可以重复获取相同的锁,synchronized和ReentrantLock都是可重入的 记录拥有锁的线程id以及获得锁的次数。再次获取的时候,如果这个锁不是它的则要等待,如果是它的话,总数+1。当释放的时候,拥有锁的个数减一。 对于synchronized而言,有多少个都行,自动释放 对于lock.lock();ReentrantLock的时候一定要手动释放锁,并且加锁次数和释放次数要一样 例如

lock.lock();
lock.lock();
lock.lock();
lock.unlock();
lock.unlock();
lock.unlock();
复制代码

作用:可重入锁是为了避免死锁。

2. 可中断锁

可中断锁就是可以响应中断的锁。在Java中,synchronized就不是可中断锁,而Lock是可中断锁。   如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。在前面演示tryLock(long time, TimeUnit unit)和lockInterruptibly()的用法时已经体现了Lock的可中断性。   

3. 公平锁

公平锁即 尽量 以请求锁的顺序来获取锁。比如,同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。而非公平锁则无法保证锁的获取是按照请求锁的顺序进行的,这样就可能导致某个或者一些线程永远获取不到锁。

  在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。而对于ReentrantLock 和 ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁   

4.自旋锁

自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。 优点: 自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快 非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能) 缺点

  1. 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
  2. 上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。

基于自旋锁,可以实现具备公平性和可重入性质的锁

TicketLock:采用类似银行排号叫好的方式实现自旋锁的公平性,但是由于不停的读取serviceNum,每次读写操作都必须在多个处理器缓存之间进行缓存同步,这会导致繁重的系统总线和内存的流量,大大降低系统整体的性能。

CLH也是一种基于单向链表(隐式创建)的高性能、公平的自旋锁,申请加锁的线程只需要在其前驱节点的本地变量上自旋,从而极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销。

public interface Lock {
    void lock();
 
    void unlock();
}
 
public class QNode {
    volatile boolean locked;
}
 
 
import java.util.concurrent.atomic.AtomicReference;
 
public class CLHLock implements Lock {
    // 尾巴,是所有线程共有的一个。所有线程进来后,把自己设置为tail
    private final AtomicReference<QNode> tail;//被final修饰的变量,不可变的是变量的引用,而不是变量的内容
    // 前驱节点,每个线程独有一个。
    private final ThreadLocal<QNode> myPred;
    // 当前节点,表示自己,每个线程独有一个。
    private final ThreadLocal<QNode> myNode;
 
    public CLHLock() {。//仅仅是初始化而已
        this.tail = new AtomicReference<QNode>(new QNode());
        this.myNode = new ThreadLocal<QNode>() {
            protected QNode initialValue() {
                return new QNode();
            }
        };
        this.myPred = new ThreadLocal<QNode>();
    }
 
    @Override
    public void lock() {
        // 获取当前线程的代表节点
        QNode node = myNode.get();
        // 将自己的状态设置为true表示获取锁。
        node.locked = true;
        // 将自己放在队列的尾巴,并且返回以前的值。第一次进将获取构造函数中的那个new QNode
        QNode pred = tail.getAndSet(node);//相当于两步操作,QNode pred=tail.get();  tail.set(node);
        // 把旧的节点放入前驱节点。
        myPred.set(pred);
        // 判断前驱节点的状态,然后走掉。
        while (pred.locked) {
        }
    }
 
    @Override
    public void unlock() {
        // unlock. 获取自己的node。把自己的locked设置为false。
        QNode node = myNode.get();
        node.locked = false;
        myNode.set(myPred.get());//可能为了突然中断,插队之类功能
    }
}
复制代码

MCSLock则是对本地变量的节点进行循环。 MCS 的实现是基于链表的,每个申请锁的线程都是链表上的一个节点,这些线程会一直轮询自己的本地变量,来知道它自己是否获得了锁。已经获得了锁的线程在释放锁的时候,负责通知其它线程,这样 CPU 之间缓存的同步操作就减少了很多,仅在线程通知另外一个线程的时候发生,降低了系统总线和内存的开销。实现如下所示:

AtomicReferenceFieldUpdater 一个基于反射的工具类,它能对指定类的指定的volatile引用字段进行原子更新。(注意这个字段不能是private的)

这步很经典

 // 这里之所以要忙等是因为:step 1执行完后,step 2可能还没执行完
    while (currentThread.next == null) { // step 5
    }
复制代码
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class MCSLock {
    public static class MCSNode {
        volatile MCSNode next;
        volatile boolean isWaiting = true; // 默认是在等待锁
    }
    volatile MCSNode queue;// 指向最后一个申请锁的MCSNode
    private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER = AtomicReferenceFieldUpdater
            .newUpdater(MCSLock.class, MCSNode.class, "queue");
    public void lock(MCSNode currentThread) {
        MCSNode predecessor = UPDATER.getAndSet(this, currentThread);// 获取队列末尾的值保存在predecessor,并将自己添加到队伍末尾
        if (predecessor != null) {//判断之前队伍是不是空的
            predecessor.next = currentThread;// 告诉前面哥们,我排在你后面,你搞完了记得告诉我一下
            while (currentThread.isWaiting) {// 等待前面哥们给自己的通知
            }
        } else { // 只有一个线程在使用锁,没有前驱来通知它,所以得自己标记自己已获得锁
            currentThread.isWaiting = false;
        }
    }

    public void unlock(MCSNode currentThread) {
        if (currentThread.isWaiting) {// 锁拥有者进行释放锁才有意义
            return;
        }

        if (currentThread.next == null) {// 检查是否有人排在自己后面
            if (UPDATER.compareAndSet(this, currentThread, null)) {// step 4
                // compareAndSet返回true表示确实没有人排在自己后面
                return;
            } else {
                // 突然有人排在自己后面了,可能还不知道是谁,下面是等待后续者!!!!!
                // 这里之所以要忙等是因为:step 1执行完后,step 2可能还没执行完
                while (currentThread.next == null) { // step 5
                }
            }
        }
        currentThread.next.isWaiting = false;
        currentThread.next = null;// for GC
    }
}
复制代码

CLHLock和MCSLock通过链表的方式避免了减少了处理器缓存同步,极大的提高了性能,区别在于CLHLock是通过轮询其前驱节点的状态,而MCS则是查看当前节点的锁状态。

CLHLock在NUMA架构下使用会存在问题。在没有cache的NUMA系统架构中,由于CLHLock是在当前节点的前一个节点上自旋,NUMA架构中处理器访问本地内存的速度高于通过网络访问其他节点的内存,所以CLHLock在NUMA架构上不是最优的自旋锁。

     

Lock接口有6个方法

lock()、tryLock()、tryLock(long time, TimeUnit unit) 和 lockInterruptibly()都是用来获取锁的 unLock()方法是用来释放锁的 newCondition() 返回 绑定到此 Lock 的新的 Condition 实例 ,用于线程间的协作

// 获取锁  
void lock()   

// 如果当前线程未被中断,则获取锁,可以响应中断  
void lockInterruptibly()   

// 返回绑定到此 Lock 实例的新 Condition 实例  
Condition newCondition()   

// 仅在调用时锁为空闲状态才获取该锁,可以响应中断  
boolean tryLock()   

// 如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁  
boolean tryLock(long time, TimeUnit unit)   

// 释放锁  
void unlock()
复制代码

用法 lock()

Lock lock = ...;//=new ReentrantLock();
lock.lock();
try{
    //处理任务
}catch(Exception ex){

}finally{
    lock.unlock();   //释放锁!
}
复制代码

tryLock()

Lock lock = ...;
if(lock.tryLock()) {
     try{
         //处理任务
     }catch(Exception ex){

     }finally{
         lock.unlock();   //释放锁
     } 
}else {
    //如果不能获取锁,则直接做其他事情
}
复制代码

lockInterruptibly(); 通过这个方法去获取锁时,如果线程 正在等待获取锁,则这个线程能够 响应中断,即中断线程的等待状态。 例如,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。 由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出 InterruptedException,但推荐使用后者 synchronized 中,当一个线程处于等待某个锁的状态,是无法被中断的

public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }  
}
复制代码

Lock的实现类 ReentrantLock Lock lock=new ReentrantLock(false); 通过参数可以设定是否为公平的锁

读写锁 ReadWriteLock lock=new ReentrantReadWriteLock(); lock.readLock().lock();

lock 与 synchronized的区别

  1. Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
  2. 在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态。
  3. 占有锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,那么其他线程就只能一直等待。Lock 可以通过:只等待一定的时间:tryLock(long time, TimeUnit unit)) 或者 能够响应中断 (解决方案:lockInterruptibly()))解决 。
  4. synchronized当多个线程都只是进行读操作时,也只有一个线程在可以进行读操作,lock通过读写锁解决问题
  5. 可以通过Lock得知线程有没有成功获取到锁 (解决方案:ReentrantLock) ,但这个是synchronized无法办到的。

lock demo

    static  class  MyThread1 implements Runnable{
        public  static  Integer num=10;
        Lock lock=new ReentrantLock();
        @Override
        public void run() {
            while (true){
                try {
                    lock.lock();
                    if(num>0){
                        System.out.println( Thread.currentThread().getName()+ "  正在卖 "+num--+" 张车票");
                    }else {
                        break;
                    }
                }finally {
                    lock.unlock();
                }

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }
    }
复制代码

Semaphore

Semaphore当前在多线程环境下被扩放使用,操作系统的信号量是个很重要的概念,在进程控制方面都有应用。Java 并发库 的Semaphore 可以很轻松完成信号量控制,Semaphore可以控制某个资源可被同时访问的个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。比如在Windows下可以设置共享文件的最大客户端访问个数。

class TestSemaphore {
    public static void main(String[] args) {
        // 线程池
        ExecutorService exec = Executors.newCachedThreadPool();
        // 只能5个线程同时访问
        final Semaphore semp = new Semaphore(3);
        // 模拟10个客户端访问
        for (int index = 0; index < 10; index++) {
            final int NO = index;
            Runnable run = new Runnable() {
                public void run() {
                    try {
                        // 获取许可
                            synchronized (this){
                                semp.acquire();
                                System.out.println(Thread.currentThread().getName()+ "Accessing: " + NO);
                                Thread.sleep((long) (Math.random() * 1000));
                                // 访问完后,释放
                                System.out.println("-----------------"+semp.availablePermits());                                
                                semp.release();//之前上句话和这句话位置互换就不对了,换成这样的到了想要的结果了

                            }

                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            exec.execute(run);
        }
        // 退出线程池
        exec.shutdown();
    }
}
复制代码

运行结果基本为这样的

pool-1-thread-1Accessing: 0
pool-1-thread-2Accessing: 1
pool-1-thread-3Accessing: 2
-----------------0
pool-1-thread-4Accessing: 3
-----------------0
pool-1-thread-5Accessing: 4
-----------------0
pool-1-thread-6Accessing: 5
-----------------0
pool-1-thread-7Accessing: 6
-----------------0
pool-1-thread-8Accessing: 7
-----------------0
pool-1-thread-9Accessing: 8
-----------------0
pool-1-thread-10Accessing: 9
-----------------0
-----------------1
-----------------2

Process finished with exit code 0
复制代码

线程池

线程的生命周期

image.png 线程池概念 线程池就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。 线程池的优点 在一个应用程序中,我们需要多次使用线程,也就意味着,我们需要多次创建并销毁线程。而创建并销毁线程的过程势必会消耗内存。而在Java中,内存资源是及其宝贵的,所以,我们就提出了线程池的概念。 多线程运行时间,系统不断的启动和关闭新线程,成本非常高,会过渡消耗系统资源,以及过渡切换线程的危险,从而可能导致系统资源的崩溃。这时,线程池就是最好的选择了

如何创建一个线程池那?

Java中已经提供了创建线程池的一个类:Executor 而我们创建时,一般使用它的子类:ThreadPoolExecutor.

package com.mianshi.test;

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

public class NewFixedThreadPoolTest {

    public static void main(String[] args) {
        // 创建一个可重用固定个数的线程池
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);//创建空闲的线程
        for (int i = 0; i < 10; i++) {
            fixedThreadPool.execute(new Runnable() {//程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。
                public void run() {
                    try {
                        // 打印正在执行的缓存线程信息
                        System.out.println(Thread.currentThread().getName()
                                + "正在被执行");
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

}
复制代码
pool-1-thread-1正在被执行
pool-1-thread-2正在被执行
pool-1-thread-3正在被执行
pool-1-thread-1正在被执行
pool-1-thread-2正在被执行
pool-1-thread-3正在被执行
pool-1-thread-1正在被执行
pool-1-thread-2正在被执行
pool-1-thread-3正在被执行
pool-1-thread-1正在被执行

因为线程池大小为3,每个任务输出打印结果后sleep 2秒,所以每两秒打印3个结果。
定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()
复制代码

线程池的任务

  1. Runnable对象可以直接扔给Thread创建线程实例,并且创建的线程实例与Runnable绑定,线程实例调用start()方法时,Runnable任务就开始真正在线程中执行。注意:如果直接调用run()方法而不调用start()方法,那么只是相当于普通的方法调用,并不会开启新的线程,程序只是在主线程中串行执行。

  2. Runnable对象也可以直接扔给线程池对象的execute方法和submit方法,让线程池为其绑定池中的线程来执行。

  3. Runnable对象也可以进一步封装成FutureTask对象之后再扔给线程池的execute方法和submit方法。

  4. Callable:功能相比Runnable来说少很多,不能用来创建线程,也不能直接扔给线程池的execute方法。但是其中的call方法有返回值。

  5. FutureTask:是对Runnable和Callable的进一步封装,并且这种任务是有返回值的,它的返回值存在FutureTask类的一个名叫outcome的数据成员中。(疑惑)那么为什么可以把没有返回值的Runnable也封装成FutureTask呢,马上我们会讨论这个问题。相比直接把Runnable和Callable扔给线程池,FutureTask的功能更多,它可以监视任务在池子中的状态。用Runnable和Callable创建FutureTask的方法稍有不同。

class NewFixedThreadPoolTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建一个可重用固定个数的线程池
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);//创建空闲的线程
        ArrayList<FutureTask<String>> list=new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            FutureTask<String> ft=new FutureTask<String>(new Callable<String>() {
                @Override
                public String call() throws Exception {
                    System.out.println(Thread.currentThread().getName());
                    return "回掉结果";
                }
            });
            list.add(ft);
            fixedThreadPool.execute(ft);
        }
        fixedThreadPool.shutdown();//不关闭线程池,主线程也不会关闭
        for (FutureTask<String> ft:list
             ) {
            System.out.println(ft.get());
        }
        if(list.get(0).get().equals("回掉结果")){
            System.out.println("开始进攻");
        }
    }
}
复制代码

运行结果

pool-1-thread-2
pool-1-thread-1
pool-1-thread-3
pool-1-thread-2
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-3
回掉结果
回掉结果
pool-1-thread-2
回掉结果
回掉结果
回掉结果
回掉结果
回掉结果
回掉结果
回掉结果
pool-1-thread-1
回掉结果
开始进攻
复制代码

猜你喜欢

转载自juejin.im/post/7033035747485712398