标题## 多线程
一. 认识线程(Thread)
一个线程就是一个 “执行流”. 每个线程之间都可以按照顺序执行自己的代码. 多个线程之间 “同时” 执行着多份代码.
基本概念:线程就是轻量级进程,共享资源.节省了申请资源的开销
为什么有线程?
进程的创销调度消耗资源太大了.
为了解决并发编程.
进程与线程的区别:
调度:线程作为处理器调度和分配的基本单位,而进程是作为拥有资源的基本单位.
同一个进程内的线程共享资源,不同的线程不共享资源/
一个进程内至少有一个线程.
为什么要有线程?
为了解决并发编程的问题.
单核CPU遇到了瓶颈.,工艺和理论没有突破的话,CPU很难继续往小做.没办法只能发展多核CPU,同时处理多个任务.提高执行速度…
有些任务场景需要等待IO,等待的时候,为了充分利用等待的时间 .
介绍一下死锁?
1.1创建进程的方式:
1 继承Thread类.
重写run方法
创建实例或者内部类调用start方法
class MyThread extends Thread {
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
}
MyThread t = new MyThread();
t.start(); // 线程开始运行
2实现Runable接口
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
}
Thread t = new Thread(new MyRunnable()); //2) 创建 Thread 类实例,
//调用 Thread 的构造方法
//时将 Runnable 对象作为 target 参数.
```t.start(); // 线程开始运行
3lambda表达式:
// 使用 lambda 表达式创建 Runnable 子类对象
Thread t3 = new Thread(() -> System.out.println("使用匿名类创建 Thread 子类对象"));
Thread t4 = new Thread(() -> {
System.out.println("使用匿名类创建 Thread 子类对象");
});
run:相当于一个任务清单,线程开始后就按照任务清单执行任务
start:相当于开始执行任务清单的内容.
4Callable接口
具有返回值的线程
重写call方法,但是Callable接口不能直接传到Thread中,需要套上一层FutrureTask
结果是FutureTask.get()会起到阻塞等待的结果,直到Callable
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class callable {
public static void main(String[] args) {
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 10; i++) {
sum+=i;
}
return sum;
}
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
Integer resut = null;
try {
resut = futureTask.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println(resut);
}
}
1.2中断一个进程:
李四一旦进到工作状态,他就会按照行动指南上的步骤去进行工作,不完成是不会结束的。但有时我们需要增加一些机制,例如老板突然来电话了,说转账的对方是个骗子,需要赶紧停止转账,那张三该如何通知李四停止呢?这就涉及到我们的停止线程的方式了。
目前常用的方法:1通过Interrupt()方法来通知
Interrupt:
1把内部的标志位设置成true;
2如果线程sleep,那就触发异常 唤醒线程
但是sleep在唤醒的时候就会把清空标志位.(sleep相当于忽略终止操作)
1. 如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通知,将中断标志设置位false
public class ThreadDemo {
private static class MyRunnable implements Runnable {
@Override
public void run() {
// 两种方法均可以
while (!Thread.interrupted()) {
//while (!Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName()
+ ": 别管我,我忙着转账呢!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println(Thread.currentThread().getName()
+ ": 有内鬼,终止交易!");
// 注意此处的 break
break;
}
}
System.out.println(Thread.currentThread().getName()
+ ": 啊!险些误了大事");
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
System.out.println(Thread.currentThread().getName()
+ ": 让李四开始转账。");
thread.start();
Thread.sleep(10 * 1000);
System.out.println(Thread.currentThread().getName()
+ ": 老板来电话了,得赶紧通知李四对方是个骗子!");
thread.interrupt();
}
}
2使用共享的标记来控制线程
public class thread {
private static boolean flag = true;
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (flag) {
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
//在这里就可以用flag控制线程是否结束.
//flag = false;
}
}
**清除标志位**
示例-3 观察标志位是否清除
标志位是否清除, 就类似于一个开关.
Thread.isInterrupted() 相当于按下开关, 开关自动弹起来了. 这个称为 "清除标志位"
Thread.currentThread().isInterrupted() 相当于按下开关之后, 开关弹不起来, 这个称为
"不清除标志位".
使用 Thread.isInterrupted() , 线程中断会清除标志位.
使用 Thread.currentThread().isInterrupted() , 线程中断标记位不会清除.
public class ThreadDemo {
private static class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.interrupted());
}
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
thread.start();
thread.interrupt();
}
}
true // 只有一开始是 true,后边都是 false,因为标志位被清
false
false
false
false
false
false
false
false
false
public class ThreadDemo {
private static class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().isInterrupted());
}
比特就业课
2.5 等待一个线程-join()
有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。例如,张三只有等李四转
账成功,才决定是否存钱,这时我们需要一个方法明确等待线程的结束。
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
thread.start();
thread.interrupt();
}
}
true // 全部是 true,因为标志位没有被清
true
true
true
true
true
true
true
true
1.3等待一个线程-join()
线程是随即调度,抢占式执行
join:等待线程执行结束再执行下面的代码.
第二个构造方法是最大等待时间.到时间就不等了:
在T1线程中调用T2.join().那么要等待T2执行结束再执行T1
publicclassThreadDemo {
publicstaticvoidmain(String[] args) throwsInterruptedException {
Runnabletarget= () -> {
for (inti=0; i<10; i++) {
try {
System.out.println(Thread.currentThread().getName()
+": 我还在工作!");
Thread.sleep(1000);
} catch (InterruptedExceptione) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() +": 我结束了!");
};
Threadthread1=newThread(target, "李四");
Threadthread2=newThread(target, "王五");
System.out.println("先让李四开始工作");
thread1.start();
thread1.join();
System.out.println("李四工作结束了,让王五开始工作"); thread2.start();
thread2.join();
System.out.println("王五工作结束了");
}
}
1.4线程的状态
NEW: 安排了工作, 还未开始行动 没有调用start()
RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.创建了PCB,真正创建线程
TERMINATED: 工作完成了.PCB释放了,但是线程Thread还在
BLOCKED: :可工作状态的时候加锁进入阻塞队列
WAITING: :可工作状态的时候,调用wair()/join()方法进入阻塞队列
TIMED_WAITING:可工作状态的时候,调用sleep()方法进入阻塞队列
BLOCKED 表示等待获取锁, WAITING 和 TIMED_WAITING 表示等待其他线程发来通知.
TIMED_WAITING 线程在等待唤醒,但设置了时限; WAITING 线程在无限等待唤醒
1.5线程安全☆
原因:
:多线程的抢占式执行,带来的随机性.
2多个线程修改同一个变量
3修改操作不是原子性 ->加锁
4内存可见性问题
5指令重排序问题
如果没有多线程,那么执行代码的顺序就是固定的,多线程就需要任何执行顺序都能有正确结果
案例:
public class Count {
public int count = 0;
public void add(){
count++;
}
@Override
public String toString() {
return "Count{" +
"count=" + count +
'}';
}
public static void main(String[] args) {
Count count = new Count();
Thread thread1 = new Thread(() -> {
for (int i = 0; i <500 ; i++) {
count.add();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i <500 ; i++) {
count.add();
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count);
}
}
结果
:Count{count=892}
进程已结束,退出代码0
线程不安全的原因:
根本原因:多线程的抢占式执行,带来的随机性.
1修改共享数据(脏读问题)
结论:++操作分为 1从内存到CPU读数据 load
2计算 add
3结果写回内存 save
,两个线程随机调度有可能读到脏数据,比如同时读取0,那么++两次的结果也是1
synchronized:修饰普通方法,静态方法,代码块,类对象.
如果一个线程对一个对象加锁,另一个线程不对这个对象加锁,也不会产生竞争而等待.
public class Count {
public int count = 0;
public void add(){
count++;
}
@Override
public String toString() {
return "Count{" +
"count=" + count +
'}';
}
public static void main(String[] args) {
Count count = new Count();
Thread thread1 = new Thread(() -> {
for (int i = 0; i <500 ; i++) {
synchronized (count){
count.add();
}
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i <500 ; i++) {
count.add();
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count);
}
}
结果:
Count{count=787}
进程已结束,退出代码0
死锁
两个或者两个以上的线程(进程)在执行过程中竞争资源而导致的永久阻塞的情况.
有可能测试没有死锁,上线就出现死锁了.
原因:
1、互斥条件:一个资源每次只能被一个进程使用;
2、请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
3、不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺
4、循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系;
1可重入锁
public synchronized void add(){
synchronized (this) {
count++;
T++;
}
}
如果允许一个线程可以对同一个对象进行加锁两次,那就是可重入的.
如果不允许一个线程可以对同一个对象进行加锁两次,那就是不可重入的.
java就是可重入锁
2两个线程两把锁,t1和t2分别针对锁A和锁B加锁,在尝试获取对方的锁.
3多个线程多把锁(哲学家就餐问题)
假设同一时刻,所有的哲学家都拿起左手的筷子,
谁也不肯让步,就进入了死锁状态.
解决哲学家问题:给锁编号,
哲学家可以拿起4和5,就不会出现死锁.
案例:
约定锁的顺序是jiangyou,cu就不会出现死锁.
public static void main(String[] args) {
Object jiangyou = new Object();
Object cu = new Object();
Count count = new Count();
Thread thread1 = new Thread(() -> {
synchronized (jiangyou) {
for (int i = 0; i < 500; i++) {
synchronized (cu){
count.add();
}
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (jiangyou) {
for (int i = 0; i < 500; i++) {
synchronized (cu){
count.add();
}
}
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count+" "+count.T);
}
volatile:
解决内存可见性和禁止指令重排序
代码在写入 volatile 修饰的变量的时候,
改变线程工作内存中volatile变量副本的值
将改变后的副本的值从工作内存刷新到主内存
代码在读取 volatile 修饰的变量的时候,
从主内存中读取volatile变量的最新值到线程的工作内存中
从工作内存中读取volatile变量的副本
static class Counter {
public int flag = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
while (counter.flag == 0) {
// do nothing
}
System.out.println("循环结束!");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("输入一个整数:");
counter.flag = scanner.nextInt();
});
t1.start();
t2.start();
]
// 当用户输入非0值时, t1 线程循环不会结束.
// (这显然是一个 bug)
内存可见性:一个线程读,一个线程改,读到的值不一定是修改之后的值. 归根结底是JVM在多线程环境下优化时产生的BUG.
t1读的时候的读的是工作内存,因为编译器优化t1没有从主内存读取,
解决问题:加volatile关键字,告诉编译器,不要优化这个变量,这个变量是易变的,
读写速度: 寄存器 > cache > 内存
工作存储区: 寄存器 +cache
wait和notify
多线程最大的问题是线程的抢占式执行,随机调度,线程在内核的调度是随机的.不能控制的.
而程序最不喜欢的就是随机了.为了控制线程的执行顺序,可以通过一些api让线程主动阻塞.放弃CPU
wait , notify ,notifyAll都是Object的方法.
wait():必须搭配synchronized来使用(必须针对同一个对象)
- 先释放锁
- 阻塞等待
- 收到通知后,重新尝试获取锁,继续往下执行
wait结束的条件:
1其他线程调用该对象的 notify 方法.
2wait 等待时间超时
3其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.
public class wait {
public static void main(String[] args) {
Object object1 = new Object();
Object object2 = new Object();
synchronized(object1){
System.out.println("wait之前");
try {
object2.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("wait之后");
}
}
}
结果:
wait之前
Exception in thread "main" java.lang.
IllegalMonitorStateException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at wait.main(wait.java:8)
进程已结束,退出代码1
notify
如果有多个线程等待则随机调度
notify之后,不会立刻释放锁,必须等待代码块执行结束之后,才能释放锁
必须先拿到锁,在进行通知wait线程.
wait和notify和synchronized必须是同一个对象才能生效.
按顺序打印:
public class wait {
public static void main(String[] args) {
Object object1 = new Object();
Object object2 = new Object();
Thread t1 = new Thread(() -> {
System.out.println("A");
synchronized (object1){
object1.notify();
}
});
Thread t2 = new Thread(() -> {
synchronized (object1){
try {
object1.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("B");
synchronized (object2){
object2.notify();
}
});
Thread t3 = new Thread(() -> {
synchronized (object2){
try {
object2.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("C");
});
t1.start();
t2.start();
t3.start();
}
}
结果:
A
B
C
BUG:有可能t2和t3的wait没有执行,先执行了notify,所以要让t1的notify启动的慢一点
修改:
t2.start();
t3.start();
Thread.sleep(500);
t1.start();
指令重排序
instance = new Singleton();的过程:
1申请内存空间
2调用构造方法,把内存空间初始化成对象
3把内存空间的值赋给instance引用
指令重排序就是123的顺序可能变成132
如果t1执行完13之后,CPU切换到t2,t2就认为instance非空,拿到的就是一个不完整的对象.
解决方式:volatile
禁止指令重排序和内存可见性
代码案例
单例模式:
有些场景中有的类只应该创建一个实例,不应该创建多个实例.
1饿汉模式
创建对象的急切感
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton(){
//隐藏构造方法
}
public Singleton getInstance(){
return instance;
}
}
2懒汉模式
使用的时候才创建实例
class SingletonLazy{
private static SingletonLazy instance = null;
public static SingletonLazy getInstance(){
if (instance == null){
instance = new SingletonLazy();
return instance;
}
return instance;
}
private SingletonLazy(){}
}
工厂模式
使用普通方法构造对象,把构造对象的细节隐藏到方法中
为什么使用共产模式: 多个构造方法是通过重载来完成的,但是如果参数一样,就无法进行重载,为了解决多个构造方法的问题,就是用共产模式
ExecutorService pool = Executors.newFixedThreadPool(10);
2.1阻塞队列
队列的特性是先进先出:
1队列为空,出队列则阻塞到队列不空为止
2队列为满,入队列则阻塞直到出队列为止.
阻塞特性:可以实现"生产者-消费者模型"
好处:1 发送方和接受方的"解耦"
AB任何一方挂了不会对对方造成影响.
2可以做到"削峰填谷",保证系统的稳定性
类似三峡大坝,上游就是 用户发送的请求,请求数是不可控的,有的时候多,有的时候少.在请求数量多的时候,阻塞队列就起到了一个缓冲的作用,避免服务器收到大规模冲击.
目标: 1会使用标准库的阻塞队列
2手写阻塞队列
1标准库:(带有阻塞功能的方法)
put:入队
take:出队
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class zuseQueue {
public static void main(String[] args) throws InterruptedException {
// BlockingQueue<String> blockingQueue = new LinkedBlockingQueue<>();
// blockingQueue.put("asd");
// String take = blockingQueue.take();
// System.out.println(take);
BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();
Thread customer = new Thread(() -> { //消费者
while (true) {
try {
Integer integer = blockingQueue.take();
System.out.println("消费元素"+integer);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
customer.start();
Thread producer = new Thread(() -> { //生产者
int count = 0;
while (true){
try {
blockingQueue.put(count);
System.out.println("生产元素:"+ count);
count++;
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producer.start();
}
}
写一个阻塞队列
前提: 阻塞队列意味着要在多线程环境下使用.所以要保证线程安全.
public class myBlockQueue {
private int[] elem = new int[100];
int head = 0;
int tail = 0;
int size = 0;
public synchronized void put(int value) throws InterruptedException {
while (size == elem.length){
this.wait();
}
elem[tail] = value;
tail++;
if (tail >= elem.length){
tail = 0;
}
size++;
this.notify(); //唤醒take的wait
}
public synchronized Integer take() throws InterruptedException {
while (size == 0){
this.wait();
}
int result = elem[head];
head++;
if (head >= elem.length){
head = 0;
}
size--;
this.notify(); //唤醒put的wait
return result;
}
}
//每次唤醒notify之后在判断一次是否满足条件
2.2定时器
1特定时刻进行提醒
2指定特定时间段之后,进行提醒
标准库:
timer.schedule():给定时器注册一个任务,在指定时间后进行执行
public class timer {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("睡觉");
}
},3000);
}
}
手写定时器:
1让注册的任务在指定的时间执行.就需要单独在定时器内搞一个线程进行周期性扫描来判断任务是否执行
2一个定时器是可以注册N个任务的,N个任务按照时间顺序进行执行.
就需要一个数据结构保存任务(堆)按照时间顺序约定优先级队列,这时扫描器只需要扫描队首元素即可.
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;
class MyTask2 implements Comparable<MyTask2>{
private Runnable runnable;
private long time;
public MyTask2(Runnable runnable, long time) {
this.runnable = runnable;
this.time = time ;
}
public long getTime() {
return time;
}
public void run(){
runnable.run();
}
@Override
public int compareTo(MyTask2 o) {
return (int) (this.getTime()-o.getTime());
}
}
public class MyTimer2 {
//扫描线程
Thread thread = null;
Object loaker = new Object();
public MyTimer2(){
thread = new Thread(() -> {
while (true){
try {
synchronized (loaker){
MyTask2 myTask = queue.take();
long curTime = System.currentTimeMillis();
if (curTime < myTask.getTime()){
queue.put(myTask);
loaker.wait(myTask.getTime() - curTime);
//有可能没到wait()cpu就调度走了,所以让整个操作变成原子操作
}else {
myTask.run();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
}
//定时器需要一个扫描线程和一个数据结构来存放任务
private PriorityBlockingQueue<MyTask2> queue = new PriorityBlockingQueue<>();
public void schedule(Runnable runnable, int after){
MyTask2 task = new MyTask2(runnable, System.currentTimeMillis() + after);
queue.put(task);
synchronized (loaker){
loaker.notify();
}
}
public static void main(String[] args) {
MyTimer2 timer2 = new MyTimer2();
timer2.schedule(new Runnable() {
@Override
public void run() {
System.out.println("1");
}
},3000);
timer2.schedule(new Runnable() {
@Override
public void run() {
System.out.println("2");
}
},5000);
}
}
3此处的优先级队列是在多线程环境下,所以要考虑线程安全
4指定堆的优先级
5如果没到时间就会重复拿出来放回去的操作(忙等)
怎么处理忙等:计算要等待的时间,之后使用wait进入阻塞状态,每次有新任务来了,就使用notify唤醒重新检测一下.,重新计算要等待的时间.
2.3线程池
因为效率是对比出来的,所以频繁创建,调度,销毁线程的时候,开销也挺大的.
如果要提高线程的效率.
1协程:更轻量的线程,
2线程池 因为创建线程,销毁线程是由操作系统完成的,
从线程池中获取和还给线程池则代码就可以完成.
一般认为,相比于内核来说,用户态自己完成的工作是高效的, 因为内核态是不可控的…
案例:
public class pool {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(10);
//创建固定数量的线程池
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
}
}
ThreadPoolExecutor★:
corePoolSize:核心线程数
maximumPoolSize: 最大线程数
keepAliveTime:临时线程不工作的时候最大存活时间
unit: 单位时间
BlockQueue workQueue : 线程池的任务队列
ThreadFactory: 用于创建线程(线程工厂)
RejectedExecutionHander: 描述了线程池的拒绝策略
AbortPolicy: 任务满了 抛出异常
CallerRunsPolicy:任务满了 , 谁加的,谁执行
DiscardPolicy:丢弃最新的任务
DiscardOldestPolicy: 丢弃最老的任务.
手写线程池:
1线程池需要一个堵塞队列保存任务
2若干个工作线程
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class MyThreadPool {
/**
* 线程池需要若干个线程 和 一个阻塞队列存放线程 任务方法
* @param n
*/
private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
public MyThreadPool(int n){
//线程数量
for (int i = 0; i < n; i++) {
Thread t = new Thread(() -> {
while (true){
try {
Runnable runnable = queue.take();
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
public void submit(Runnable runnable) {
//将任务交给线程池
try {
queue.put(runnable);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
MyThreadPool myThreadPool = new MyThreadPool(10);
for (int i = 0; i <1000 ; i++) {
int n = i;
myThreadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println("线程:"+Thread.currentThread().getName()+" "+n);
}
});
}
}
}
3.1锁策略
1乐观锁和悲观锁
乐观锁: 预测锁竞争不会很激烈
悲观锁:预测锁竞争会很激烈
2轻量级锁和重量级锁
轻量级锁:加锁,解锁开销比较小,效率更高
重量级锁: 加锁,解锁,开销比较大,效率更低
3自旋锁和挂机等待锁
自旋锁: 是一种轻量级锁,
第一时间获取锁被释放,但占用大量系统资源
挂起等待锁: 是一种重量级锁.
节省CPU,不占用大量资源.获取锁的时机会更迟,
4互斥锁和读写锁
互斥锁: 加锁和解锁两个操作
读写锁:针对读加锁和针对写加锁
因为多线程读一个变量是线程安全的
5公平锁和非公平锁
公平锁:按照先来后到来竞争锁
6可重入锁和不可重入锁
一个线程可以加锁多次不会发生死锁.
synchronzied: 既是悲观锁,也是乐观锁.
既是轻量级锁,也是重量级锁.
默认是轻量级锁,发现锁竞争比较激烈,就会转为重量级锁
的轻量级锁是用自旋锁的方式实现的
的重量级锁是用挂起等待的方式实现的
是非公平锁
是不是读写锁
是可重入锁
3.2CAS
比较和交换
if(V == A) swap(V,B); 原子操作
原子类: atomicInteger 来解决线程安全问题
实现自旋锁 就是通过不间断的比较来确认是否是否锁
3.3synchronzied
1锁膨胀/锁升级
1 无锁
2偏向锁:第一个尝试加锁的线程, 优先进入偏向锁状态.
不是真正的加锁,只是做个标记.有需要再加锁
3轻量级锁
4重量级锁
当synchronized发生锁竞争的时候,就从偏向锁升级为轻量级锁,
这时的锁是自旋锁,自旋到一定程度的时候.就会升级为重量级锁.
附件
1Thraed.isAlive():PCB不存在就代码这个线程不存在(NEW ,TERMINATED)
2为什么要有static描述类属性?
因为这是传承C++的,早期的内存中有一个静态内存区,
staiic修饰的就放入静态方法区,后来静态方法区消失了,static又被赋予了新的含义.
3为什么两个线程并发执行,执行的速度不是单线程的一半?
不能保证是并行(两个核心执行),
线程的调度自身也有时间消耗
4不保证原子性的后果
比如两个线程操作一个数据,一个线程执行到一半开始执行另一个线程,
读到的数据就是脏数据,影响最终结果.
5synchronized加锁操作时把一串指令打包成一个原子的.
这个加锁操作也是原子的,所以不会出现加锁加到一半后出现CPU调度的情况.
6死锁
6.1死锁是咋回事
2两个线程两把锁,t1和t2分别针对锁A和锁B加锁,在尝试获取对方的锁.
6.2死锁的三个典型情况
可重入锁:一个线程对一个对象加锁两次不产生死锁就是可重入锁,
产生死锁就是不可重入锁
**两个线程两把锁:**两个线程分别占用对应的资源又申请对方的资源,而资源只有1个,从而产生死锁.
多个线程多把锁:(哲学家问题)
解决方案:给锁编号
6.3死锁4个必要条件
互斥,请求和保持,不可剥夺,循环等待
6.4如何破除死锁?
1给锁编号
2线程执行前申请全部资源
7synchronized不能修饰变量,只能修饰方法和代码块
8wait和sleep的区别
1wait是Object的方法,sleep是Thread的方法
2wait和notify唤醒是正常唤醒,sleep被唤醒是带有异常的.
3wait需要和synchronized一起使用,sleep不需要
9饿汉模式和懒汉模式在多线程下是否是线程安全的?
饿汉在多线程下只有读操作,不涉及线程安全问题
懒汉既有读,又有写,所以在多线程下是不安全的.
本质上懒汉模式是有可能出现脏读问题,要把读写原子化.
class SingletonLazy{
private static volatile SingletonLazy instance = null;
public static SingletonLazy getInstance(){
if (instance == null) {
synchronized (SingletonLazy.class) {
if (instance == null){
instance = new SingletonLazy();
return instance;
}
}
}
return instance;
}
private SingletonLazy(){}
}
10变量捕获
要求:代码中没有修改这个变量就可以捕获.
public class pool {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(10);
//创建固定数量的线程池
for (int i = 0; i <1000 ; i++) { // i修改了 不能捕获 n没有修改,可以捕获
int n = i; //变量捕获
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello"+n);
}
});
}
}
}
i是在主线程的栈上,有可能会随着主线程的结束而销毁,
为了防止作用域的差异,在调用run()方法的时候i已经销毁
所以要让run方法把主线程的i拷贝一份到自己的栈上```
11线程池一般应该创建几个线程?
具体问题具体分析.
CPU密集型: 进行一系列算数运算,线程数量不应该超过CPU核数
IO密集型:大多数时间处于阻塞状态(读写硬盘,等待用户输入),那就可以多创建一些线程.,
但实际中没有理想情况,只有通过实践/测试来找合适的线程数量
关键字
原子性
可见性:一个线程对共享变量值的修改,能够及时地被其他线程看到.
synchronized可以保证原子性
volatile只能保证内存可见性
static class Counter {
public int flag = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
while (true) {
synchronized (counter) {
if (counter.flag != 0) {
break;
}
}
// do nothing
}
System.out.println("循环结束!");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("输入一个整数:");
counter.flag = scanner.nextInt();
});
t1.start();
t2.start();
}
可以去掉synchronized 加上volatile
12ABA问题
CA运行的核心是检查value和oldvalue是否一致,一致就当没有被修改过.可以进行下一步操作.
就是取钱 在ATM上取钱,账户 有1000 取500 多按了一次.
使用CAS比较现在的值和原来的值的时候,又有人转了500,结果就扣款成功了.
解决方案: 加个版本号
13CAS能干什么
1实现原子类
2实现自旋锁
14reentrantLock(可重入锁)
和synchronized有什么不一样? ReentrantLoack使用了lock方法和unlock方法来加锁解锁.
reentrantLock有最大等待时间,synchronized没有竞争到锁就会死等
reentrantLock提供了公平锁的实现,
reentrantLock可以唤醒指定线程(使用Condition类)
15 Semaphore(信号量)
可用的资源数
类似于停车场
Semaphore semaphore = new Semaphore(2);
semaphore.acquire();
semaphore.acquire();
semaphore.acquire();
semaphore.release();
16如何将不线程安全的类线程同步?
1加锁
2Collections.synchronized方法
3写实拷贝 copyonwriteArrayList
多线程直接修改就拷贝一份新的数据进行修改,读就读旧的数据,最后将新的数据替换旧的数据(只适用与数据比较小的情况)
.
17多线程使用哈希表★
HashMap是线程不安全的,
HashTable是线程安全的,给关键方法加了synchronized
推荐使用:concurrentHashMap
concurrentHashMap进行了什么优化?
1concurrentHashMap缩小了冲突概率,把一把大锁转成多把小锁.
HashTable是直接给哈希表加锁,只要操作哈希表,那就可能产生锁冲突.
Hashtable锁冲突的概率就太大了,任何两个元素的操作都会发生锁冲突
在同一个链表下多线程进行修改有可能产生线程安全问题,因为要修改next值就可能出现风险 .
如果在不同的链表下修改那是不需要加锁的.
concurrentHashtable是把链表的头节点作为锁对象.只有针对同一个链表进行操作才会有锁竞争
jdk1.8之前是几个链表共用一个锁
jdk1.8之后就是一个链表一个锁
2concurrentHashMap只针对写加锁 即读和写直接不加锁
使用volatile和原子的写操作来保证不出现脏读问题
3concurrentHashMap内部充分使用了CAS,进一步降低了加锁的数量.
比如元素的个数
4针对扩容,采取化整为零的方式
扩容一般是把这个数组上的链表搬运到新的数组(删除和插入)
如果数据数量太多就消耗时间
concurrentHashMap达到阈值后,每次添加操作会添加到新的数组,同时把旧的元素搬运一部分到新的数组.