Java SE 学习笔记(十五)—— 多线程(2)

1 线程池

1.1 线程状态

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。线程对象在不同的时期有不同的状态。

状态被定义在了 java.lang.Thread.State 枚举类中,State枚举类的源码如下:

public class Thread {
    public enum State {
    
        /* 新建 */
        NEW , 

        /* 可运行状态/就绪状态 */
        RUNNABLE , 

        /* 阻塞状态 */
        BLOCKED , 

        /* 无限等待状态 */
        WAITING , 

        /* 计时等待 */
        TIMED_WAITING , 

        /* 终止 */
        TERMINATED;
    
	}
    
    // 获取当前线程的状态
    public State getState() {
        return jdk.internal.misc.VM.toThreadState(threadStatus);
    }
}

通过源码我们可以看到 Java 中的线程存在 6 种状态,每种线程状态的含义如下:

在这里插入图片描述

各个状态的转换,如下图所示:
在这里插入图片描述

注意:

  • 线程处于运行状态的时候与系统里的CPU产生了关系,虚拟机是没有定义运行状态的

1.2 线程池基本原理

在这里插入图片描述

从上图可以看到,以前写多线程的弊端:用到线程的时候就创建,用完线程就消失

线程池可以看做成一个池子,在该池子中存储很多个线程。

线程池存在的意义:

​系统创建一个线程的成本是比较高的,因为它涉及到与操作系统交互,当程序中需要创建大量生存期很短暂的线程时,频繁的创建和销毁线程对系统的资源消耗有可能大于业务处理是对系统资源的消耗。

针对这一种情况,为了提高性能,我们就可以采用线程池。线程池在启动的时,会创建大量空闲线程,当我们向线程池提交任务的时,线程池就会启动一个线程来执行该任务。等待任务执行完毕以后,线程并不会死亡,而是再次返回到线程池中称为空闲状态。等待下一次任务的执行。

1.3 创建线程池

JDK对线程池也进行了相关的实现,在真实企业开发中我们也很少去自定义线程池,而是使用JDK中自带的线程池。

我们可以使用 Executors中 所提供的 静态 方法来创建线程池

  • static ExecutorService newCachedThreadPool() :创建一个默认的线程池
  • static newFixedThreadPool(int nThreads):创建一个指定最多线程数量的线程池
  1. 创建默认的线程池
public static void main(String[] args) throws InterruptedException {

	//创建一个默认的线程池对象.池子中默认是空的.默认最多可以容纳int类型的最大值.
	ExecutorService executorService = Executors.newCachedThreadPool();
	//Executors --- 可以帮助我们创建线程池对象
	//ExecutorService --- 可以帮助我们控制线程池

	executorService.submit(()->{
		System.out.println(Thread.currentThread().getName() + "在执行了");
	});
	// 若不加以下代码,线程1还没来得及归还到线程池就要创建另一个线程,线程2
	// 若加上以下代码,线程1执行完成后(在线程睡眠过程中)归还到线程池,睡眠结束后,发现线程池中还有刚刚归还的线程1,再次创建线程时,不需要创建新的线程只需要拿出线程池中的线程1即可
	//Thread.sleep(2000);

	executorService.submit(()->{
		System.out.println(Thread.currentThread().getName() + "在执行了");
	});

	executorService.shutdown();
}
  1. 创建一个指定最多线程数量的线程池
public static void main(String[] args) {
	//参数不是初始值而是最大值
	ExecutorService executorService = Executors.newFixedThreadPool(10);
	ThreadPoolExecutor pool = (ThreadPoolExecutor) executorService;
	System.out.println(pool.getPoolSize());//0

	executorService.submit(()->{
		System.out.println(Thread.currentThread().getName() + "在执行了");
	});

	executorService.submit(()->{
		System.out.println(Thread.currentThread().getName() + "在执行了");
	});

	System.out.println(pool.getPoolSize());//2
//        executorService.shutdown();
}
  1. 自定义创建线程池对象

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(核心线程数量,最大线程数量,空闲线程最大存活时间,任务队列,创建线程工厂,任务的拒绝策略);

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
                          // 什么时候拒绝任务:当提交的任务>池子中最大线程数量+队列容量
                          // 如何拒绝:
  
/*  
corePoolSize:   核心线程的最大值,不能小于0
maximumPoolSize:最大线程数,不能小于等于0,maximumPoolSize >= corePoolSize
keepAliveTime:  空闲线程最大存活时间,不能小于0
unit:           时间单位
workQueue:      任务队列,不能为null
threadFactory:  创建线程工厂,不能为null      
handler:        任务的拒绝策略,不能为null */ 

注意:明确线程池对多可执行的任务数 = 队列容量 + 最大线程数

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "在执行了");
    }
}
public class test {
    public static void main(String[] args) {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(2,5,2,TimeUnit.SECONDS,new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());

        pool.shutdown();
    }
}

1.4 任务拒绝策略

RejectedExecutionHandler 是jdk提供的一个任务拒绝策略接口,它下面存在4个子类。

  • ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常,是 默认的策略
  • ThreadPoolExecutor.DiscardPolicy: 丢弃任务,但是不抛出异常 这是不推荐的做法。
  • ThreadPoolExecutor.DiscardOldestPolicy: 抛弃队列中等待最久的任务 然后把当前任务加入队列中。
  • ThreadPoolExecutor.CallerRunsPolicy: 调用任务的 run() 方法绕过线程池(交给主线程)直接执行。

2 共享变量的问题与解决

2.1 存在的问题

当A线程修改了共享数据时,B线程没有及时获取到最新的值,如果还在使用原先的值,就会出现问题。

总结:

  • 堆内存是唯一的,每一个线程都有自己的线程栈。
  • 每一个线程在使用堆里面变量的时候,都会先拷贝一份到变量的副本中。
  • 在线程中,每一次使用是从变量的副本中获取的。

2.2 Volatile解决

Volatile关键字:强制线程每次在使用变量的时候,都会看一下共享区域最新的值

public class Money {
    public static volatile int money = 100000;// 结婚基金已经不是十万了
    //public static int money = 100000;
    //如果不加volatile关键字,首先执行线程1,会一直执行while循环,程序无法终止
    //线程1虽然知道基金是十万,但是当基金的余额发生变化的时候,无法知道最新的余额。
}
public class MyThread1 extends  Thread {
    @Override
    public void run() {
        while(Money.money == 100000){
		//什么都不做
        }
        System.out.println("基金已经不是十万了");
    }
}
public class MyThread2 extends Thread {
    @Override
    public void run() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Money.money = 90000;
    }
}
public class Demo {
    public static void main(String[] args) {
        MyThread1 t1 = new MyThread1();
        t1.setName("小路同学");
        t1.start();

        MyThread2 t2 = new MyThread2();
        t2.setName("小皮同学");
        t2.start();
    }
}

2.3 synchronized解决

  • 线程获得锁
  • 清空变量副本
  • 拷贝共享变量最新的值到变量副本中
  • 执行代码
  • 将修改后变量副本中的值赋值给共享数据
  • 释放锁
public class Money {
    public static Object lock = new Object();
    public static int money = 100000;
}
public class MyThread1 extends  Thread {
    @Override
    public void run() {
        while(true){
            synchronized (Money.lock){
                if(Money.money != 100000){
                    System.out.println("结婚基金已经不是十万了");
                    break;
                }
            }
        }
    }
}
public class MyThread2 extends Thread {
    @Override
    public void run() {
        synchronized (Money.lock) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Money.money = 90000;
        }
    }
}
public class Demo {
    public static void main(String[] args) {
        MyThread1 t1 = new MyThread1();
        t1.setName("小路同学");
        t1.start();

        MyThread2 t2 = new MyThread2();
        t2.setName("小皮同学");
        t2.start();
    }
}

3 原子性

所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可以分割的整体。

3.1 原子性的实现(synchronized)

public class MyAtomThread implements Runnable {
    private int count = 0; //送冰淇淋的数量
    //private volatile int count = 0;
    //private Object lock = new Object();

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            //count++;会做以下三件事:
            //1,从共享数据中读取数据到本线程栈中.
            //2,修改本线程栈中变量副本的值
            //3,会把本线程栈中变量副本的值赋值给共享数据.
            //synchronized(lock){
	            count++;
	            System.out.println("已经送了" + count + "个冰淇淋");
           // }
        }
    }
}
public class AtomDemo {
    public static void main(String[] args) {
        MyAtomThread atom = new MyAtomThread();

        for (int i = 0; i < 100; i++) {
            new Thread(atom).start();
        }
    }
}

注意:

  • 该程序最后的结果可能不会是“已经送了10000个冰淇淋”,那是因为 count++ 不是一个原子性操作,也就是说其在执行的过程中,有可能被其他线程打断操作
  • volatile 关键字只能保证每次在使用共享数据的时候是最新值,当其中一个线程改变了副本变量值还未写回共享区域的时候,其他线程获取的最新值还是未改变的,所以volatile关键字不能保证原子性
  • 使用synchronized代码块,创建锁对象可以保证原子性,将共享数据锁起来,一次只能有一个线程执行共享数据

3.2 AtomicInteger

Java 从 JDK1.5 开始提供了 java.util.concurrent.atomic 包(简称Atomic包),这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式

  1. 构造方法
  • public AtomicInteger():初始化一个默认值为0的原子型 Integer
  • public AtomicInteger(int initialValue):初始化一个指定值的原子型 Integer
  1. 常用方法
  • int get():获取值
  • int getAndIncrement():以原子方式将当前值加1,注意,这里返回的是自增前的值。
  • int incrementAndGet():以原子方式将当前值加1,注意,这里返回的是自增后的值。
  • int addAndGet(int data):以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
  • int getAndSet(int value):以原子方式设置为newValue的值,并返回旧值。
import java.util.concurrent.atomic.AtomicInteger;

public class test {
    public static void main(String[] args) {
        AtomicInteger ac1 = new AtomicInteger();
        AtomicInteger ac2 = new AtomicInteger(100);
        int i1 = ac1.get();
        int i2 = ac2.get();
        System.out.println(i1); //0
        System.out.println(i2); //100

        int andIncrement = ac2.getAndIncrement();
        System.out.println(andIncrement); //100
        System.out.println(ac2.get()); //101

        int incrementAndGet = ac2.incrementAndGet();
        System.out.println(incrementAndGet); //102
        System.out.println(ac2.get()); //102

        int addAndGet = ac2.addAndGet(30);
        System.out.println(addAndGet); //132
        System.out.println(ac2.get()); //132

        int andSet = ac2.getAndSet(10);
        System.out.println(andSet); //132
        System.out.println(ac2.get()); //10
    }
}
  1. AtomicInteger原理

继续 3.1 中的例子,使用AtomicInteger实现原子性

import java.util.concurrent.atomic.AtomicInteger;

public class MyAtomThread implements Runnable {
    AtomicInteger ac = new AtomicInteger(0);

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            int count = ac.incrementAndGet();//相当于把count++;的三步变成一个整体
            System.out.println("已经送了" + count + "个冰淇淋");
           
        }
    }
}
public class AtomDemo {
    public static void main(String[] args) {
        MyAtomThread atom = new MyAtomThread();

        for (int i = 0; i < 100; i++) {
            new Thread(atom).start();
        }
    }
}

AtomicInteger原理: 自旋锁 + CAS 算法

CAS算法:

  • 有3个操作数(内存值V, 旧的预期值A,要修改的值B)
  • 当旧的预期值A == 内存值 此时修改成功,将V改为B
  • 当旧的预期值A!=内存值 此时修改失败,不做任何操作,并重新获取现在的最新值(这个重新获取的动作就是 自旋

CAS算法总结:在修改共享数据的时候,把原来的旧值记录下来了。

  • 如果现在内存中的值跟原来的旧值一样,证明没有其他线程操作过内存值,则修改成功。
  • 如果现在内存中的值跟原来的旧值不一样了,证明已经有其他线程操作过内存值了。
    则修改失败,需要获取现在最新的值,再次进行操作,这个重新获取就是自旋

3.3 悲观锁和乐观锁

synchronized 和 CAS 的区别 :

  • 相同点:在多线程情况下,都可以保证共享数据的安全性。
  • 不同点:
    • synchronized总是从最坏的角度出发,认为每次获取数据的时候,别人都有可能修改。所以在每次操作共享数据之前,都会上锁。(悲观锁
    • CAS是从乐观的角度出发,假设每次获取数据别人都不会修改,所以不会上锁。只不过在修改共享数据的时候,会检查一下,别人有没有修改过这个数据。(乐观锁
      • 如果别人修改过,那么我再次获取现在最新的值。
      • 如果别人没有修改过,那么我现在直接修改共享数据的值

4 并发工具类

4.1 Hashtable

在集合类中 HashMap 是比较常用的集合对象,但是 HashMap 是线程不安全的(多线程环境下可能会存在问题)。为了保证数据的安全性我们可以使用 Hashtable,但是 Hashtable 的效率低下。

import java.util.HashMap;

public class MyHashMapDemo {
    public static void main(String[] args) throws InterruptedException {
        //HashMap<String, String> hm = new HashMap<>();
        Hashtable<String, String> hm = new Hashtable<>();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 25; i++) {
                hm.put(i + "", i + "");
            }
        });


        Thread t2 = new Thread(() -> {
            for (int i = 25; i < 51; i++) {
                hm.put(i + "", i + "");
            }
        });

        t1.start();
        t2.start();

        System.out.println("----------------------------");
        Thread.sleep(1000);

        //0-0 1-1 ..... 50- 50

        for (int i = 0; i < 51; i++) {
            System.out.println(hm.get(i + ""));
        }//0 1 2 3 .... 50
    }
}

总结:

  • Hashtable采取悲观锁 synchronized 的形式保证数据的安全性
  • 只要有线程访问,会将整张表全部锁起来,所以Hashtable的效率低下

4.2 ConcurrentHashMap

  • HashMap是线程不安全的。多线程环境下会有数据安全问题
  • Hashtable是线程安全的,但是会将整张表锁起来,效率低下
  • ConcurrentHashMap也是线程安全的,效率较高。(在JDK7和JDK8中,底层原理不一样)

Map接口体系结构:
在这里插入图片描述

  1. ConcurrentHashMap1.7原理

在这里插入图片描述

注意:

  • 当两个元素挂在同一个小数组的同一位置,旧元素会挂在新元素的后面
  • 小数组将要添加第二个索引位置的元素之前会扩容再添加
  1. ConcurrentHashMap1.8原理

底层结构:哈希表。( 数组、链表、红黑树的结合体)

结合CAS机制+ synchronized同步代码块形式保证线程安全。

在这里插入图片描述

总结 :

  • 如果使用空参构造创建ConcurrentHashMap对象,则什么事情都不做。 在第一次添加元素的时候创建哈希表
  • 计算当前元素应存入的索引。
  • 如果该索引位置为null,则利用cas算法,将本结点添加到数组中。
  • 如果该索引位置不为null,则利用volatile关键字获得当前位置最新的结点地址,挂在他下面,变成链表。
  • 当链表的长度大于等于8时,自动转换成红黑树6,以链表或者红黑树头结点为锁对象,配合悲观锁保证多线程操作集合时数据的安全性

4.3 CountDownLatch

使用场景:让某一条线程等待其他线程执行完毕之后再执行

在这里插入图片描述

三个孩子线程

import java.util.concurrent.CountDownLatch;

public class ChileThread1 extends Thread {

    private CountDownLatch countDownLatch;
    public ChileThread1(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        //1.吃饺子
        for (int i = 1; i <= 10; i++) {
            System.out.println(getName() + "在吃第" + i + "个饺子");
        }
        //2.吃完说一声
        //每一次countDown方法的时候,就让计数器-1
        countDownLatch.countDown();
    }
}
import java.util.concurrent.CountDownLatch;

public class ChileThread2 extends Thread {

    private CountDownLatch countDownLatch;
    public ChileThread2(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }
    @Override
    public void run() {
        //1.吃饺子
        for (int i = 1; i <= 15; i++) {
            System.out.println(getName() + "在吃第" + i + "个饺子");
        }
        //2.吃完说一声
        //每一次countDown方法的时候,就让计数器-1
        countDownLatch.countDown();
    }
}
import java.util.concurrent.CountDownLatch;

public class ChileThread3 extends Thread {

    private CountDownLatch countDownLatch;
    public ChileThread3(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }
    @Override
    public void run() {
        //1.吃饺子
        for (int i = 1; i <= 20; i++) {
            System.out.println(getName() + "在吃第" + i + "个饺子");
        }
        //2.吃完说一声
        //每一次countDown方法的时候,就让计数器-1
        countDownLatch.countDown();
    }
}

一个妈妈线程

import java.util.concurrent.CountDownLatch;

public class MotherThread extends Thread {
    private CountDownLatch countDownLatch;
    public MotherThread(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        //1.等待
        try {
            //当计数器变成0的时候,会自动唤醒这里等待的线程。
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //2.收拾碗筷
        System.out.println("妈妈在收拾碗筷");
    }
}

测试类

import java.util.concurrent.CountDownLatch;

public class MyCountDownLatchDemo {
    public static void main(String[] args) {
        //1.创建CountDownLatch的对象,需要传递给四个线程。
        //在底层就定义了一个计数器,此时计数器的值就是3
        CountDownLatch countDownLatch = new CountDownLatch(3);
        //2.创建四个线程对象并开启他们。
        MotherThread motherThread = new MotherThread(countDownLatch);
        motherThread.start();

        ChileThread1 t1 = new ChileThread1(countDownLatch);
        t1.setName("小明");

        ChileThread2 t2 = new ChileThread2(countDownLatch);
        t2.setName("小红");

        ChileThread3 t3 = new ChileThread3(countDownLatch);
        t3.setName("小刚");

        t1.start();
        t2.start();
        t3.start();
    }
}

4.4 Semaphore

使用场景:可以控制访问特定资源的线程数量

import java.util.concurrent.Semaphore;

public class MyRunnable implements Runnable {
    //1.获得管理员对象,允许几个线程同时运行
    private Semaphore semaphore = new Semaphore(2);
    @Override
    public void run() {
        //2.获得通行证
        try {
            semaphore.acquire();
            //3.开始行驶
            System.out.println("获得了通行证开始行驶");
            Thread.sleep(2000);
            System.out.println("归还通行证");
            //4.归还通行证
            semaphore.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}
public class MySemaphoreDemo {
    public static void main(String[] args) {
        MyRunnable mr = new MyRunnable();

        for (int i = 0; i < 100; i++) {
            new Thread(mr).start();
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值