21. java之多线程
2021/8/3 20:07:33
本文主要是介绍21. java之多线程,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
二、多线程
2.1 多线程的创建方式
1)继承Thread类
2)实现Runable接口
3)实现Callable接口
/** * callable方式创建线程 * 返回值 * * 线程间的通讯(wait/notify) */ public class Test1 { public static void main(String[] args) { // //开启一个子线程 // //继承Thread类 // new Thread(){ // @Override // public void run() { // // } // }.start(); // // //继承Runable接口 // new Thread(() -> { // // }).start(); System.out.println("主线程开始执行"); //开启子线程 FutureTask<String> futureTask = new FutureTask<>(new MyCallable()); new Thread(futureTask).start(); //主线程进行其他的业务处理 try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } //获取子线程的返回值 try { String result = futureTask.get(); System.out.println("获得子线程的返回值:" + result); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } } class MyCallable implements Callable<String> { @Override public String call() throws Exception { System.out.println("子线程开始执行" + Thread.currentThread().getName()); Thread.sleep(5000); System.out.println("子线程执行结束" + Thread.currentThread().getName()); return "Hello"; } }
4)线程池
2.2 线程的生命周期
新建状态 -> 就绪状态 -> 运行状态 <-> 阻塞状态 -> 死亡状态
2.3 线程安全问题
2.3.1 什么是线程安全问题?
在多线程环境下,当n个线程同时的操作(增删改)一份共享资源时,因为线程调度的不确定性,可能引起资源状态前后的不一致,这种问题就是线程安全问题
2.3.2 实际开发工程中如何判断是否有线程安全的问题?
要能够明确 竞态资源是否存在,是什么资源
2.3.3 线程安全的引发原因
引起线程不安全的原因在于:
1、可见性
2、原子性
3、有序性
任何一个业务,如果没有保证以上3个特性中的任意一个特性,就有线程安全的问题
1)可见性
一个线程对一份资源的修改,对其他线程必须立即可见
可见性的案例
public class Test2 { public static boolean flag = true; public static void main(String[] args) { System.out.println("主线程开始执行!"); //开启子线程 new Thread(){ @Override public void run() { try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } flag = false; System.out.println("子线程重新设置flag变量:" + flag); } }.start(); while(flag){ } System.out.println("主线程执行结束!"); } }
2)原子性(实际开发过程中大概率都是因为原子性的问题)
一个操作是一个整体不可分割的
比如:
i = 10;//原子性
i++; //非原子性 x=i+1; | i=x;
j = 10.0;//在32位的系统上非原子,在64位的系统上是原子性注意:两个原子性的操作放在一起,就不是原子性的了
public class Test3 { private static int i = 0; public static void main(String[] args) { for (int j = 0; j < 1000; j++) { new Thread(() -> { i++; }).start(); } try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("i的结果:" + i); } }
3)有序性
指令重排:cpu为了优化考虑,可能会打乱代码的执行顺序。
指令重拍的保证:cpu可以保证代码在单线程情况下,指令重拍后,不影响原程序的执行结果。
public class Test3 { private static boolean flag = true; private static Object obj; public static void main(String[] args) { //线程1 while(flag){ } int i = obj.hashCode(); //线程2 obj = new Object(); flag = false; } }
2.3.4 如何解决线程安全的问题
1)使用volatile关键字
volatile关键字可以保证变量的可见性,以及局部有序性。
volatile不能保证原子性。使用volatile关键字修饰的变量,一旦被某个线程修改,其他线程是立刻可见的。
2)使用JUC(java.util.concurrent)包下的AtomicXxxx类来保证原子性
AtomicXxxx类里面的方法都是原子操作
ublic class Test3 { private static int i = 0; private static volatile AtomicInteger atomicInteger = new AtomicInteger(i); public static void main(String[] args) { for (int j = 0; j < 1000; j++) { new Thread(() -> { atomicInteger.getAndIncrement();//i++ }).start(); } try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("i的结果:" + atomicInteger.get()); } }
3)使用AtomicXxxx类提供的cas方法
CAS(compare and swap)本质上就是乐观锁操作
/** * flag.compareAndSet(0, 1) * 判断flag的值是否为0,如果为0就修改成1,并且返回true,如果不为0就不修改,并且返回false * 这个过程是一个原子不可分割的操作 */ public class Test4 { private static String name;//姓名 private static int age;//年龄 private static AtomicInteger flag = new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { new Thread(() -> { boolean result = flag.compareAndSet(0, 1); if (result) { name = "小红"; try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } age = 18; } }).start(); new Thread(() -> { boolean result = flag.compareAndSet(0, 1); if (result) { name = "小明"; try { Thread.sleep(499); } catch (InterruptedException e) { e.printStackTrace(); } age = 17; } }).start(); new Thread(() -> { boolean result = flag.compareAndSet(0, 1); if (result) { name = "小刚"; try { Thread.sleep(498); } catch (InterruptedException e) { e.printStackTrace(); } age = 19; } }).start(); Thread.sleep(1000); System.out.println(name + " " + age); } }
4)使用synchronized关键字加锁
synchronized关键字,可以保证可见性、原子性、有序性
可见性:解锁时,会强制的将所有变量刷新到主存中
原子性:加锁后,其他线程无法获得锁,也就不能打断线程中的程序执行
有序性:原子性保证了,有序性就保证了锁什么东西?- 重要
1、尽可能选择细粒度更小的对象上锁
2、synchronized修饰普通方法时,默认锁this对象
3、synchronized修饰静态方法时,默认锁当前类的class对象
5)使用Lock对象加锁
JDK1.4提供的新的加锁方式,JDK1.5之后,对synchronized关键字做了一个优化,性能和Lock就差不多了。所以现在还是有很多程序员喜欢使用synchronized关键词
重入锁
//重入锁 Lock lock = new ReentrantLock(); new Thread(() -> { lock.lock(); System.out.println("线程1执行"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程1执行结束"); lock.unlock(); }).start(); new Thread(() -> { lock.lock(); System.out.println("线程2执行"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程2执行结束"); lock.unlock(); }).start();
读写锁
package com.qf.demo14; import java.util.concurrent.locks.ReentrantReadWriteLock; public class Test7 { public static void main(String[] args) { //读写锁 ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); //读锁 ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock(); //写锁 ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock(); new Thread(() -> { readLock.lock(); System.out.println("线程1执行"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程1执行结束"); readLock.unlock(); }).start(); new Thread(() -> { writeLock.lock(); System.out.println("线程2执行"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程2执行结束"); writeLock.unlock(); }).start(); } }
读写锁:
读锁 兼容 读锁
读锁 不兼容 写锁
写锁 不兼容 写锁
6)使用数据库的锁保证数据的一致性(表锁、行锁(共享锁、排他锁))
7)使用Redis的Lua脚本保证数据一致
…
2.3.5 编写一个线程安全的懒汉式的单例模式
CAS版
public class Test8 { private Test8(){} private static AtomicReference<Test8> reference = new AtomicReference<>(); public static Test8 getInstance(){ reference.compareAndSet(null, new Test8()); return reference.get(); } }
双重锁判定
public class Test8 { private Test8(){} private volatile static Test8 test8; public static Test8 getInstance(){ if(test8 == null){ synchronized (Test8.class) { if(test8 == null){ //线程1 test8 = new Test8(); //初始化对象 //1、申请堆内存空间 //2、初始化申请的堆内存空间 //3、将变量test8指向堆内存空间 } } } return test8; } }
2.3.6 死锁
Object obj = new Object(); Object obj2 = new Object(); new Thread(() -> { System.out.println("线程1执行。。。。"); synchronized (obj) { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (obj2){ } } System.out.println("线程1结束。。。。"); }).start(); new Thread(() -> { System.out.println("线程2执行。。。。"); synchronized (obj2) { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (obj){ } } System.out.println("线程2结束。。。。"); }).start();
2.3.7 集合中的线程安全
1)ArrayList、HashMap等都是线程不安全的集合,那么,多线程中使用这些线程不安全的集合会有什么问题?
这些基础集合,添加元素时,因为是多线程,同时没有任何锁机制,所以很大的概率发生一些元素覆盖或者丢失的情况。多线程同时读写也会造成一定的问题,比如读到写线程的中间状态,造成业务判定问题等等
2)如何解决多线程操作中,集合不安全的问题?
在多线程环境下,可以使用一个线程安全的集合,比如Vector、Hashtable。但是Vector和Hashtable的锁的细粒度太大了,对于高并发的读写性能损耗太高,实际开发过程中,并不推荐使用。建议采用JDK1.4之后推出的JUC包中提供的一些线程安全的集合,比如ConcurrentHashMap等,这些集合在保证线程安全的同时,也尽可能的提高了并发能力。
思考:
1、是不是实际开发过程中就一定不能用ArrayList这种线程不安全的集合?- 不是,具体问题具体分析
2、使用了线程安全的集合,是否就意味着不会发生线程安全问题呢?- 所谓的线程安全集合,只是指里面的每个单独的方法是线程安全的,但是将这些方法组合起来形成的业务,并不能保证线程安全
3)CopyOnWriteArrayList - 线程安全版的ArrayList
CopyOnWriteArrayList 采用重入锁 + 写入时复制的手段,保证集合的线程安全。添加的时候加锁,写入时无需加锁(写入时复制的方式),以此来提高集合的读取的效率。
CopyOnWriteArrayList特别适合读多写少的场景,并发读取是没有任何锁机制,但是写入的成本会将对较高,每次写入都需要拷贝一新的数组。并且这种方式,可能使得读取的数据有一定概率是旧数据,所以如果程序允许这种短时间内的不一致性(最终一致性),CopyOnWriteArrayList是非常合适的,但是如果程序必须要求数据的绝对一致性,这时应该采用List list = Collections.synchronizedList(new ArrayList<>());这种方式获得线程安全的集合
写入时复制
public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); } }
4)ConcurrentHashMap - 线程安全版的HashMap
底层实现
JDK1.7之前,采用分段锁的方式保证线程安全
Jdk1.8之后,采用CAS + synchronized 来保证线程安全(比JDK1.7锁的细粒度更小)
源码分析
添加元素的方法(put)
final V putVal(K key, V value, boolean onlyIfAbsent) { //key和value不能为null if (key == null || value == null) throw new NullPointerException(); //调用哈希函数,计算key的哈希值 int hash = spread(key.hashCode()); //1、标志位 //2、链表的长度 int binCount = 0; //循环 - 自旋 //tab指向底层的哈希表 for (Node<K,V>[] tab = table;;) { //f - 添加元素对应的哈希桶的第一个元素 //n - 哈希表的容量 //i - 添加元素计算出来的下标(对应的哈希桶的位置) //fh - 标志位 Node<K,V> f; int n, i, fh; //判断哈希表是否初始化 if (tab == null || (n = tab.length) == 0) //哈希表还未初始化,进行初始化的操作 //initTable方法是线程安全的,可以保证只有一个线程能够初始化哈希表 tab = initTable(); //i = (n - 1) & hash 计算新增元素的哈希桶的位置,赋值给i变量 //tabAt(tab, i = (n - 1) & hash) 等价于 tab[i = (n - 1) & hash] //因为哈希表本身是保证可见性的,但是哈希表中的元素并不保证可见性, //tabAt(tab, i = (n - 1) & hash)该方法是通过内存地址直接去内存中获取元素(保证可见性) //其实就是从哈希表中获得桶i的第一个元素赋值给f,判断是否为null else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //没有发生哈希碰撞 //开始讲新的值封装成Node节点,放入该桶的位置 //通过CAS的方式判断桶i的位置是否有元素,如果没有就赋值(原子性),如果有的话,就停止操作,进入下一轮for循环,继续自旋 if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin } //获取桶i位置的第一个元素f哈希值,赋值给fh //如果fh==-1,则表示该桶i正在进行扩容的迁移 else if ((fh = f.hash) == MOVED) //当前线程保证扩容的线程,迁移桶i的元素 //迁移完成后,返回最新的哈希表 tab = helpTransfer(tab, f); else { //说明当前桶i有元素,并且没有在迁移 //新的元素就可以放入桶i的位置,但是需要解决哈希碰撞的问题 V oldVal = null; //进行加锁,锁桶i的第一个元素f //一个桶一个锁,最大化的降低了锁的细粒度 synchronized (f) { //双重锁判定,加锁之后再判断一次条件 if (tabAt(tab, i) == f) { //fh>0,说明当前桶i是一个链表 if (fh >= 0) { //因为是链表,binCount设置为1 //在链表循环的过程中,binCount代表着链表的长度 binCount = 1; for (Node<K,V> e = f;; ++binCount) { K ek; //判断key是否重复 if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e; if ((e = e.next) == null) { //将新的元素放入链表的尾部 //尾插 pred.next = new Node<K,V>(hash, key, value, null); break; } } } //判断当前桶i是否为一颗红黑树 else if (f instanceof TreeBin) { //走红黑树的逻辑 Node<K,V> p; //如果是红黑树,binCount会被设置为2 binCount = 2; if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } } if (binCount != 0) { //binCount = 1 链表 //binCount = 2 红黑树 if (binCount >= TREEIFY_THRESHOLD) //判断是否需要转成红黑树 treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } //哈希表的扩容 addCount(1L, binCount); return null; }
初始化哈希表
//初始化哈希表 //可能有n个线程同时执行该方法 private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; //自旋 //如果哈希表为空,就走循环体,如果哈希表不为空,就直接返回哈希表对象 while ((tab = table) == null || tab.length == 0) { //如果sc<0 说明当前已经有线程在初始化哈希表了,当前线程进入让步状态 if ((sc = sizeCtl) < 0) Thread.yield(); // lost initialization race; just spin //通过cas的方式,设置sizeCtl为-1表示已经有一个线程进行初始化哈希表了 else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { //双重判定 if ((tab = table) == null || tab.length == 0) { int n = (sc > 0) ? sc : DEFAULT_CAPACITY; //初始化哈希表 @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; //将哈希表赋值给全局变量 table = tab = nt; sc = n - (n >>> 2); } } finally { sizeCtl = sc; } break; } } return tab; }
2.3.8 线程池
参数介绍
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
参数一:corePoolSize , 核心线程数,线程池的最小线程数量
参数二:maximumPoolSize,最大线程数,如果线程池的线程不够用时,会创建新的线程,但是不会超过这个数量
参数三:keepAliveTime,线程最大的空闲时间,超过核心线程数的线程,超过空闲时间后,就会被自动回收
参数四:unit,空闲时间的单位
参数五(重要):workQueue,线程池的阻塞队列对象(类型)
参数六:threadFactory,线程创建的工厂对象(决定了线程的创建方式)
参数七:handler,多余的任务(Runnable)如果无法放入阻塞队列,该用什么方式拒绝该任务(4种方式)
2.4 线程间的通讯
2.4.1 什么是线程间的通讯?
因为多线程环境下,线程调度有确定性,但是有时候的业务,需要一个线程基于另外一个线程的结果才能继续进行,这时就需要考虑线程间通讯的问题了
2.4.2 线程间通讯的方式
1、wait/notify/notifyAll
2、阻塞队列
2.4.3 Wait/Notify
1、wait和notify必须写在同步代码块(同步方法)中
2、必须有同步锁对象调用wait和notify方法问题:
1、wait和sleep的区别?- wait会释放锁资源,sleep不会释放
2.4.4 阻塞队列
add - 如果队列满了,添加元素会报错
put - 如果队列满了,添加元素会阻塞
poll - 如果没有元素,直接返回null
take - 如果没有元素,会阻塞当前的线程
2.4.5 线程间通讯的实际运用场景
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vJm1sTSn-1627987591497)(img/image-20200804160457464.png)]
**面试题:**有ABCD4个线程,A线程只能写A,B线程只能写B,以此类推。请编写一个程序,通过ABCD4个线程输出4个文件1,2,3,4。第一个文件中只有ABCDABCD, 第二个文件中输出BCDABCDA,第三个文件中输出CDABCDAB,第四个文件中输出DABCDABC
这篇关于21. java之多线程的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-27消息中间件底层原理资料详解
- 2024-11-27RocketMQ底层原理资料详解:新手入门教程
- 2024-11-27MQ底层原理资料详解:新手入门教程
- 2024-11-27MQ项目开发资料入门教程
- 2024-11-27RocketMQ源码资料详解:新手入门教程
- 2024-11-27本地多文件上传简易教程
- 2024-11-26消息中间件源码剖析教程
- 2024-11-26JAVA语音识别项目资料的收集与应用
- 2024-11-26Java语音识别项目资料:入门级教程与实战指南
- 2024-11-26SpringAI:Java 开发的智能新利器