JUC并发编程
文章目录
- JUC并发编程
- JUC简介
- 进程、线程的关系
- Java默认有两个线程
- 线程的六种状态
- 并发、并行的关系:
- Lock锁
- Synchronized 和 Lock的区别
- 8锁现象
- 问题一 : 在标准情况下,两个线程先打印 发短信 还是 打电话 ?
- 问题二 : 在发短信方法中,延迟4秒,两个线程先打印 发短信 还是 打电话?
- 问题三 : Phone类增加一个普通方法,线程B调用,那么两个线程先打印 发短信 还是 打电话?
- 问题四 : 创建两个 phone对象,线程调用不同对象的方法,那么两个线程先打印 发短信 还是 打电话?
- 问题五 : 将方法变为静态同步方法,那么两个线程先打印 发短信 还是 打电话?
- 问题六 : 现在有两个对象,调用不同对象的,那么两个线程先打印 发短信 还是 打电话?
- 问题七 : 资源类中,一个是静态同步方法,一个普通同步方法,那么两个线程先打印 发短信 还是 打电话?
- 问题八: 在标准情况下,两个线程先打印 发短信 还是 打电话 ?
- 总结:
- 集合类不安全
- 创建线程
- JUC 常用的辅助类
- 我的学习论坛
JUC并发编程
JUC简介
来源于 java.util.concurrent、java.util.concurrent.atomic、java.util.concurrent.locks 这三个包(简称JUC ),在此包中增加了在并发编程中很常用的实用工具类,用于定义类似于线程的自定义子系统,包括线程池、异步 IO 和轻量级任务框架。提供可调的、灵活的线程池。还提供了设计用于多线程上下文中的 Collection 实现等。
进程、线程的关系
进程:一个程序,QQ.exe Music.exe 程序的集合。
线程:一个进程往往可以包含多个线程,至少包含一个!
Java默认有两个线程
mian 线程
GC 线程
线程的六种状态
新生 NEW
运行 RUNNABLE
阻塞 BLOCKED等待
死死地等 WAITING
超时等待 TIMED_WAITING
终止 TERMINATED
并发、并行的关系:
并发(多线程操作同一个资源)
并行:(多个人一起行走) CPU 多核 ,多个线程可以同时执行;
并发编程的本质:充分利用CPU的资源
Lock锁
Lock锁是一个接口,其所有的实现类为:
ReentrantLock(可重入锁)
ReentrantReadWriteLock.ReadLock(可重入读写锁中的读锁)
ReentrantReadWriteLock.WriteLock(可重入读写锁中的写锁)
Lock lock = new ReentrantLock();
public void doTicket(){
lock.lock(); //加锁
try {
System.out.println(Thread.currentThread().getName());
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock(); // 解锁
}
}
Synchronized 和 Lock的区别
来源不同
synchronize => java的内置关键字,在jvm层;Lock =>java的一个接口
获取锁得方式不同
synchronize => 自动获取锁,不能判断锁得状态;Lock => 手动获取锁,可判断是否获取到锁
线程阻塞方面
synchronize => 线程1阻塞会导致线程2永远等待;Lock=>不一定会等下去
锁得类型不同
synchronize=>可重入锁、不可中断、非公平;Lock=>可重入锁、可判断锁、非公平(可设置成公平)
使用范围不同
synchronize=>适用于少量代码块同步;Lock=>适合锁大量的同步代码块
8锁现象
8锁现象,实际对应的就是8个问题。
掌握了这8个问题后:可以清楚判断锁的是谁!永远的知道什么是锁,锁到底锁的是谁!
问题一 : 在标准情况下,两个线程先打印 发短信 还是 打电话 ?
public class Test1 {
public static void main(String[] args) throws Exception {
Phone phone = new Phone();
// 线程A
new Thread(()->{phone.seedMsg();}, "A").start();
// 4秒延迟
TimeUnit.SECONDS.sleep(4);
// 线程B
new Thread(()->{phone.call();}, "B").start();
}
}
class Phone{
public synchronized void seedMsg(){
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
}
结果是:先打印发短信,然后再打电话!
问题二 : 在发短信方法中,延迟4秒,两个线程先打印 发短信 还是 打电话?
public class Test2 {
public static void main(String[] args) throws Exception {
Phone phone = new Phone();
// 线程A
new Thread(()->{phone.seedMsg();}, "A").start();
// 1秒延迟
TimeUnit.SECONDS.sleep();
// 线程B
new Thread(()->{phone.call();}, "B").start();
}
}
class Phone{
public synchronized void seedMsg() throws Exception{
TimeUnit.SECONDS.sleep(4);
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
}
结果:还是先打印发短信。
首先知道 锁的对象是谁?因为 synchronized 加在方法上,所以锁的对象是 方法的调用者,所以两个方法用的是同一个锁,谁先拿到谁先执行!
解释:
phone对象,就是方法的调用者,也就是手机,它可以打电话和发短信。
现在有两个人线程A 和 线程B,他们一个想打电话,一个想发短信。
线程A,先拿到锁(也就是手机),抱着锁(手机)睡了4秒。
线程B肯定拿不到锁(手机),需要等待。
问题三 : Phone类增加一个普通方法,线程B调用,那么两个线程先打印 发短信 还是 打电话?
public class Test3 {
public static void main(String[] args) throws Exception {
Phone phone = new Phone();
// 线程A
new Thread(()->{ phone.seedMsg();}, "A").start();
// 1秒延迟
TimeUnit.SECONDS.sleep(1);
// 线程B
new Thread(()->{ phone.hello();}, "B").start();
}
}
class Phone{
//同步方法
public synchronized void seedMsg(){
// 1秒延迟
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
//普通方法
public void hello(){
System.out.println("hello");
}
}
结果:先打印hello,然后打印发短信。
解释:
锁的是方法的调用者
现在线程B 调用普通方法,相当于可以远程操控,不需要接收消息
问题四 : 创建两个 phone对象,线程调用不同对象的方法,那么两个线程先打印 发短信 还是 打电话?
public class Test4 {
public static void main(String[] args) throws Exception {
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(()->{ phone1.seedMsg(); }, "A").start();
// 1秒延迟
TimeUnit.SECONDS.sleep(1);
new Thread(()->{ phone2.call(); }, "B").start();
}
}
class Phone{
//同步方法
public synchronized void seedMsg(){
// 4秒延迟
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
//同步方法
public synchronized void call(){
System.out.println("打电话");
}
//普通方法
public void hello(){
System.out.println("hello");
}
}
结果:先打印打电话。
解释:synchronized用在方法上,那么锁的是方法的调用者。现在有两个调用者,所以互不影响。
问题五 : 将方法变为静态同步方法,那么两个线程先打印 发短信 还是 打电话?
问题六 : 现在有两个对象,调用不同对象的,那么两个线程先打印 发短信 还是 打电话?
public class Test5 {
public static void main(String[] args) throws Exception {
Phone phone = new Phone();
// 线程A
new Thread(()->{ phone.seedMsg();}, "A").start();
// 1秒延迟
TimeUnit.SECONDS.sleep(1);
// 线程B
new Thread(()->{ phone.call(); }, "B").start();
// 问题6
// Phone phone1 = new Phone();
// Phone phone2 = new Phone();
// new Thread(()->{ phone1.seedMsg();}, "A").start();
// new Thread(()->{ phone2.call();}, "B").start();
}
}
class Phone{
// 静态同步方法
public static synchronized void seedMsg(){
// 4秒延迟
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
// 静态同步方法
public static synchronized void call(){
System.out.println("打电话");
}
}
结果:先打印发短信。
解释:5、6的问题一样,对于static静态方法来说,对于整个类Class只有一份,对于不同的对象使用的是同一份方法,相当于这个方法是属于这个类,如果静态static方法使用synchronized锁定,那么这个synchronized锁会锁住整个对象!不管多少个对象,对于静态的锁都只有一把锁,谁先拿到这个锁就先执行,其他的进程都需要等待!
问题七 : 资源类中,一个是静态同步方法,一个普通同步方法,那么两个线程先打印 发短信 还是 打电话?
public class Test7 {
public static void main(String[] args) throws Exception {
Phone phone = new Phone();
new Thread(()->{ phone.seedMsg(); }, "A").start();
// 1秒延迟
TimeUnit.SECONDS.sleep(1);
new Thread(()->{ phone.call(); }, "B").start();
}
}
// 手机
class Phone{
// 静态同步方法
public static synchronized void seedMsg(){
// 4秒延迟
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
// 普通同步方法
public synchronized void call(){
System.out.println("打电话");
}
}
结果:先打印打电话,然后打印发短信
解释:因为一个锁的是Class类,一个锁的是对象调用者。后面那个打电话不需要等待发短信,可以直接运行。
问题八: 在标准情况下,两个线程先打印 发短信 还是 打电话 ?
public class Test8 {
public static void main(String[] args) throws Exception{
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(()->{ phone1.seedMsg(); }, "A").start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{ phone2.call(); }, "B").start();
}
}
class Phone{
// 静态同步方法
public static synchronized void seedMsg(){
// 4秒延迟
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
// 普通同步方法
public synchronized void call(){
System.out.println("打电话");
}
}
结果:先打印打电话,然后打印发短信
解释:两把锁锁的不是同一个东西,所以后面的第二个对象不需要等待第一个对象的执行。
总结:
new 和 this 是一个对象。
static 和 Class 是唯一的一个模板。
集合类不安全
List不安全
ArrayList 在并发情况下是不安全的!
解决:
Vector就是线程安全的
使用Collections.synchronizedList(new ArrayList<>());
使用 List arrayList = new CopyOnWriteArrayList<>();
CopyOnWriteArrayList:写入时复制, 是计算机程序设计领域的一种优化策略
多个线程调用的时候,list,读取的时候,固定的,写入(存在覆盖操作);在写入的时候避免覆盖,造成数据错乱的问题;
Set不安全
和List、Set同级的还有一个BlockingQueue 阻塞队列;
Set和List同理可得: 多线程情况下,普通的Set集合是线程不安全的;
解决:
使用Collections工具类的synchronized包装的Set类
使用CopyOnWriteArraySet 写入复制的JUC解决方案
Map不安全
HashMap基础类也存在并发修改异常!
解决:
使用Collections.synchronizedMap(new HashMap<>());处理;
使用ConcurrentHashMap进行并发处理
创建线程
之前创建线程:
new Thread(()->{ System.out.println("创建handsome线程");},"handsome").start();
Callable创建线程:
Java库具有FutureTask类型,该类型实现Runnable和Future,并方便地将两种功能组合在一起。可以通过为其构造函数提供Callable来创建FutureTask。然后,将FutureTask对象提供给Thread的构造函数以创建Thread对象。因此,间接地使用Callable创建线程。
注意:使用Callable进行多线程操作,多个线程结果会被缓存,效率高。这个get 方法可能会产生阻塞!把他放到 最后,或者使用异步通信来处理!
Callable接口与Runnable接口的区别:
是否有返回值
是否抛出异常
一个是call(),一个是run()
JUC 常用的辅助类
CountDownLatch
这个类使一个线程等待其他线程各自执行完毕后再执行。
主要方法
countDown 减一操作。
await 等待计数器归零。
public static void main(String[] args) throws InterruptedException {
//总数6个
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <= 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName() +" 执行do");
//每个线程都数量-1
countDownLatch.countDown();
},String.valueOf(i)).start();
}
//等待计数器归零
countDownLatch.await();
System.out.println("必须其他线程都执行完,在执行这里");
//最后执行的...
}
CyclickBarrier
用于对多个线程任务进行同步执行。
主要方法
await 在所有线程任务都到达之前,线程任务都是阻塞状态
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("召唤神龙的线程~");
});
for (int i=1;i<=7;i++){
int atI = i;
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+" 收集了第" + atI +"颗龙珠");
cyclicBarrier.await(); //加法计数 等待
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
},"线程"+i).start();
}
}
应用:
CyclickBarrier可以根据基于子线程进行处理其他线程的结果,处理比较复杂的业务。并且可以通过reset方法重新执行方法。
CountDownLoatch则必须在主线程才能处理,一般用于任务执行初始化数据
Semaphore
信号量,在信号量定义两种操作:
acquire(获取)当一个线程调用acquire操作,它通过成功获取信号量(信号量-1),有阻塞,直到有线程释放信号量,或者超时。
release(释放)实际上将信号量的值+1,然后唤醒等待的线程。
public static void main(String[] args) {
//停车位为3个
Semaphore semaphore = new Semaphore(3);
// 参数设为1即相当于Synchronized,即可设置占用锁的时间。
// Semaphore semaphore = new Semaphore(1);
for (int i=1 ; i<=10; i++){
int atI = i;
new Thread(()->{
try {
semaphore.acquire(); //得到
System.out.println(Thread.currentThread().getName() + " 抢到停车位" + atI);
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + " 离开停车场");
} catch (Exception e) {
e.printStackTrace();
} finally {
semaphore.release(); //释放
}
},"线程"+i).start();
}
}
作用: 多个共享资源互斥的使用! 并发限流,控制最大的线程数!
读写锁ReadWriteLock
读写锁:更加细粒度的锁
读-读:可以共存
读-写:不能共存
写-写:不能共存
JUC的目的,就是将锁的粒度变的更细,提高并发效率;至少读-读,可以共存
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache mycache = new MyCache();
//开启5个线程 写入数据
for (int i = 1; i <=5 ; i++) {
int finalI = i;
new Thread(()->{
mycache.put(String.valueOf(finalI),String.valueOf(finalI));
}).start();
}
//开启10个线程去读取数据
for (int i = 1; i <=10 ; i++) {
int finalI = i;
new Thread(()->{
String o = mycache.get(String.valueOf(finalI));
}).start();
}
}
}
class MyCache{
private volatile Map<String,String> map = new HashMap<>();
//普通锁
//private Lock lock = new ReentrantLock();
//使用读写锁
private ReadWriteLock lock = new ReentrantReadWriteLock();
public void put(String key,String value){
//写锁
lock.writeLock().lock();
try {
//写入
System.out.println(Thread.currentThread().getName()+" 线程 开始写入");
map.put(key, value);
System.out.println(Thread.currentThread().getName()+" 线程 写入完成");
} finally {
lock.writeLock().unlock();
}
}
public String get(String key){
//读锁
lock.readLock().lock();
String o;
try {
System.out.println(Thread.currentThread().getName()+" 线程 开始读取");
o = map.get(key);
System.out.println(Thread.currentThread().getName()+" 线程 读取完成");
} finally {
lock.readLock().unlock();
}
return o;
}
}
对于读取,我们运行多个线程同时读取,也能在一定程度上提高效率。
阻塞队列
ArrayBlockingQueue : 一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue : 一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue : 一个支持优先级排序的无界阻塞队列。
DelayQueue: 一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue: 一个不存储元素的阻塞队列。
LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque: 一个由链表结构组成的双向阻塞队列。
线程池
优点:
降低资源消耗
提高响应速度
提高线程的可管理性
池化技术:事先准备好一些资源,如果有人要用,就来我这里拿,用完之后还给我,以此来提高效率。
线程池的使用
Executors:
// 创建线程池
public static void main(String[] args) {
// 单个线程
// ExecutorService executorService = Executors.newSingleThreadExecutor();
// 创建一个固定的线程池的大小
// ExecutorService executorService = Executors.newFixedThreadPool(5);
// 可伸缩的
ExecutorService executorService = Executors.newCachedThreadPool();
try {
for (int i=1; i<=80; i++){
//使用线程池之后创建线程
executorService.execute(()->{
System.out.println(Thread.currentThread().getName()+ " ok");
});
}
} finally {
executorService.shutdown();
}
}
源码分析:线程池的真正实现类是ThreadPoolExecutor,有7大参数
阿里巴巴开发手册
ThreadPoolExecutor
自定义创建线程池,参数类型就是7大参数,分别是 核心线程池大小、最大的线程池大小、超时了没有人调用就会释放、超时单位、阻塞队列、线程工厂 创建线程的 一般不用动、拒绝策略
public static void main(String[] args) {
// 注意:
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 5, 3, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy() //拒绝策略
);
try {
for (int i=1; i<=80; i++){
//使用线程池之后创建线程
threadPoolExecutor.execute(()->{
System.out.println(Thread.currentThread().getName()+ " ok");
});
}
} finally {
threadPoolExecutor.shutdown();
}
}
等待队列已经满了,塞不下新任务,同时,线程池中max线程也达到了,无法继续新任务。这个时候就需要拒绝策略机制
拒绝策略有4种:
AbortPolicy:如果阻塞队列满了,直接抛出异常阻止系统正常运行,队列容量大小 + maxPoolSize
CallerRunsPolicy:如果阻塞队列满了,该策略不会抛弃任务,也不抛出异常,而是将任务回退给调用者
DiscardPolicy:如果阻塞队列满了,丢弃无法处理的任务,不抛出异常,如果允许任务丢失,是最好的策略
DiscardOldestPolicy:如果阻塞队列满了,抛弃队列中等待最久的任务,把当前任务加入队列中再次提交
如何去设置线程池的最大大小如何去设置?
CPU密集型:电脑的核数是几核就选择几;选择maximunPoolSize的大小Runtime.getRuntime().availableProcessors() // 获取CPU核数
I/O密集型:在程序中有15个大型任务,io十分占用资源;I/O密集型就是判断我们程序中十分耗I/O的线程数量,大约是最大I/O数的一倍到两倍之间。
Java内置核心四大函数式接口
我的学习论坛
HandsomeForum:用Java编写的学习论坛,打造我们自己的圈子!(http://huangjunjie.vip:66)
文章链接:http://huangjunjie.vip:66/blog/read/q6s628g3bfcg79mhug
更多推荐
所有评论(0)