【狂神说Java】多线程详解笔记
线程状态
java类的方式
- 外部类: 在主函数所在的类的外部定义的类, 若是在同一文件, 直接new; 若是不同文件, 导包再new
- 静态内部类: 在主函数所在的类的内部且在主函数外部定义的类, 直接new
因为main是static, 所以他也要是static
- 局部类: 在主函数内定义的类, 直接new
- 匿名内部类:
Interface o = new Interface(){类的定义}
, 需先定义interface lambda:
Runnable as = () -> { System.out.println("as"); }; as.run();
new Thread(()->System.out.println("as")).start();
线程休眠sleep
Thread.sleep(1000);
单位为毫秒
线程礼让yield
- 调用方式: 在线程内
Thread.yield();
将线程由运行态转为就绪态, 此时重新决定哪个线程获取cpu, 可能还是刚才的进程
package com.zq; public class yield { public static void main(String[] args) { myYield y = new myYield(); new Thread(y, "a").start(); new Thread(y, "b").start(); } } class myYield implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName() + "->start"); Thread.yield(); System.out.println(Thread.currentThread().getName() + "->end"); } }
join
- 阻塞其他线程, 强制该线程运行, 类似插队
调用:
thread.join();
注意是线程对象, 不是类package com.zq; public class join implements Runnable { public static void main(String[] args) throws InterruptedException { join j = new join(); Thread thread = new Thread(j); thread.start(); for (int i = 0; i < 10; i++) { if (i == 5) { thread.join(); } System.out.println("main->" + i); } } @Override public void run() { for (int i = 0; i < 20; i++) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("vip->" + i); } } }
线程优先级
默认为5, main线程优先级为5
守护线程
- 用户线程执行完后,虚拟机即关闭,尽管守护线程没有执行完毕
- 使用:
thread.setDaemon("true");
默认为false
线程同步
- 线程同步形成条件: ==队列+锁==
- sleep不会释放锁
synchronized
- 同步方法默认锁的对象是this
- 可以用同步块完全替代同步方法
死锁 活锁 饥饿
死锁: 两个或更多线程阻塞着等待其它处于死锁状态的线程所持有的锁
- 形象的例子: 两个小朋友分别拿着对方喜欢的玩具, 而且谁都不愿先把手中的玩具先给对方
- 死锁通常发生在多个线程同时但以不同的顺序请求同一组锁的时候
- 死锁会让你的程序挂起无法完成任务
- 解决方法: 只能通过中止并重启的方式来让程序重新执行
- 在程序中, 双方不会协商, 只会一直僵持, 程序一直阻塞
死锁的四个必要条件:
1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
预防死锁--破坏死锁的四个必要条件
破坏互斥条件:使资源同时访问而非互斥使用,就没有进程会阻塞在资源上,从而不发生死锁。
破坏请求和保持条件:采用静态分配的方式,静态分配的方式是指进程必须在执行之前就申请需要的全部资源,且直至所要的资源全部得到满足后才开始执行,只要有一个资源得不到分配,也不给这个进程分配其他的资源。
破坏不剥夺条件:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源,但是只适用于内存和处理器资源。
破坏循环等待条件:给系统的所有资源编号,规定进程请求所需资源的顺序必须按照资源的编号依次进行。
活锁: 活锁指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败, 线程为了彼此间的响应而相互礼让,使得没有一个线程能够继续前进
- 形象的例子: 两条车道上, 两人在同一车道相向行走, 发现对方阻碍自己的道路时, 两人都向另一条车道偏移, 结果还是会阻碍对方的道路(传说中的神默契), 这样一直僵持
- 活锁有可能自行解开
- 活锁可以认为是一种特殊的饥饿
饥饿: 是指一个可运行的进程尽管能继续执行,但被调度器无限期地忽视,而不能被调度执行的情况
- 饥饿可以通过先来先服务资源分配策略来避免
- 优先级高的线程抢占资源, 导致优先级低的线程一直得不到资源
- 某个线程长期占用资源, 导致其他线程得不到资源
lock
private ReentrantLock lock=new ReentrantLock();
lock.lock();
try {
//放并发操作代码
System.out.println(Thread.currentThread().getName() + "-->抢到第" + ticketNum-- + "张票");
} finally {
lock.unlock();
}
- 性能比ssynchronized好
- 一般放在try-finally中, 不然容易出问题
线程通信
线程池
package com.zq;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class pool {
public static void main(String[] args) {
MyThread mt = new MyThread();
ExecutorService service = Executors.newFixedThreadPool(2);
long startTime = System.currentTimeMillis(); //获取开始时间(毫秒)
for (int i = 0; i < 50; i++) {
service.execute(mt);
}
service.shutdown();
long endTime = System.currentTimeMillis(); //获取结束时间(毫秒)
System.out.println("程序运行时间: " + (endTime - startTime) + "ms");
}
}
class MyThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
如果线程池大小小于开启的线程数, 则等待之前的线程执行完毕释放, 再执行新线程
summary
package com.zq;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Summary {
public static void main(String[] args) {
new MyThread1().start();
new Thread(new MyThread2()).start();
FutureTask<Integer> ft = new FutureTask<Integer>(new MyThread3());
new Thread(ft).start();
try {
Integer integer = ft.get();
System.out.println(integer);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class MyThread1 extends Thread {
@Override
public void run() {
System.out.println("1继承Thread");
}
}
class MyThread2 implements Runnable {
@Override
public void run() {
System.out.println("2实现Runnable");
}
}
class MyThread3 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("3实现callable");
return 520;
}
}
一些扩展
- java中的锁Lock就是基于AbstractQueuedSynchronizer来实现的
- 在大多数情况下,我们写并发代码使用synchronized就足够了,而且使用synchronized也是首选
- 但是lock更加灵活
-
- lockInterruptibly方法可以响应中断,lock方法会阻塞线程直到获取到锁,而tryLock方法则会立刻返回,返回true代表获取锁成功,而返回false则说明获取不到锁
- newCondition方法返回一个条件变量,一个条件变量也可以做线程间通信来同步线程。多个线程可以等待在同一个条件变量上,一些线程会在某些情况下通知等待在条件变量上的线程,而有些变量在某些情况下会加入到条件变量上的等待队列中去。
- 独占锁就是只能有一个线程获取到锁,其他线程必须在这个锁释放了锁之后才能竞争而获得锁
- 共享锁则可以允许多个线程获取到锁
ReentrantLock
- 是lock的子类
- 可重入性: 同一个线程可以多次获得锁,而不同线程依然不可多次获得锁
划分:
- 公平锁: 保证等待时间最长的线程将优先获得锁
- 非公平锁: 并不会保证多个线程获得锁的顺序,并发性能表现更好,ReentrantLock默认使用非公平锁
CopyOnWriteArrayList
- ArrayList的线程安全版本
- CopyOnWriteArrayList是在有写操作的时候会copy一份数据,然后写完再设置成新的数据。CopyOnWriteArrayList适用于读多写少的并发场景
- CopyOnWriteArraySet是线程安全版本的Set实现,它的内部通过一个CopyOnWriteArrayList来代理读写等操作,使得CopyOnWriteArraySet表现出了和CopyOnWriteArrayList一致的并发行为
- 使用了ReentrantLock来支持并发操作