虽然多进程也能实现并发编程, 但是线程比进程更轻量:所以线程又叫轻量级进程;
创建线程比创建进程更快;
销毁线程比销毁进程更快;
调度线程比调度进程更快;
Tset就是进程,main是主线程,此时多余的一个进程java.exe是idea自己
总结:
注意事项:
1.启动线程是start方法,不是run方法。通过start方法将线程启动以后,每个线程自动执行自己的run方法
2.Thread-0这些线程名字是默认的,可以修改
3.这四个线程同时执行,互不影响
继承Thread的子类就是一个线程实体
// 定义一个Thread类,相当于一个线程的模板
class MyThread01 extends Thread {
// 重写run方法
// run方法描述的是线程要执行的具体任务
@Overridepublic
void run() {
System.out.println("hello, thread.");
}
}
/**
* 继承Thread类并重写run方法创建一个线程
* @author rose
* @created 2022-06-20
*/
public class Thread_demo01 {
public static void main(String[] args) {
// 实例化一个线程对象
MyThread01 t = new MyThread01();
// 真正的去申请系统线程,参与CPU调度
t.start();
}
}
这个实现Runnable接口的子类,并不是真正的的线程实体,只是线程的一个核心工作任务。这是和第一种方法最大的区别
// 创建一个Runnable的实现类,并实现run方法
// Runnable主要描述的是线程的任务
class MyRunnable01 implements Runnable {
@Overridepublic void run() {
System.out.println("hello, thread.");
}
}
/**
* 通过实现Runnable接口并实现run方法
* @author rose
* @created 2022-06-20
*/
public class Thread_demo02 {
public static void main(String[] args) {
// 实例化Runnable对象
MyRunnable01 runnable01 = new MyRunnable01();
// 实例化线程对象并绑定任务
Thread t = new Thread(runnable01);
// 真正的去申请系统线程参与CPU调度
t.start();
}
}
1.创建线程任务对象
2.创建线程对象,并传入任务对象
3.调用Thread类的start方法启动线程
1.两种方式创建线程最后都是通过Thread类的start方法启动线程
2.继承Thread类方法创建线程属于单继承,有局限性
3.实现Runnable接口的子类更加灵活,不仅实现Runnable接口,还可以继承其他类
4.调用当前线程的区别:继承Thread类,直接使用this就表示当前线程对象的引用;实现Runnable 接口, this 表示的是 MyRunnable 的引用. 需要使用 Thread.currentThread()
/**
* @author hide_on_bush
* @date 2022/7/12
*/
public class OtherMethod {
public static void main(String[] args) {
//1.匿名内部类继承Thread类
Thread thread=new Thread(){
@Override
public void run() {
System.out.println("这是匿名内部类继承Thread类");
System.out.println(Thread.currentThread().getName());
}
};
//2.匿名内部类实现Runnable接口
Thread runThread=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("这是匿名内部类实现Runnable接口");
System.out.println(Thread.currentThread().getName());
}
});
thread.start();
runThread.start();
System.out.println("这是主线程"+Thread.currentThread().getName());
}
}
lambda表达式是建立在函数式接口,只有一个抽象方法!!!
//3.Lambad
Thread lambadaThread=new Thread(()-> System.out.println("这是Lambda表达式实现Runnable接口"));
理论上:并发执行速度是顺序执行的一倍,所以串行耗时应该是并发的一倍
实际上:线程的创建、销毁和调用也会耗时,所以实际的时间比理论实践多一点
正解:可能先打印1也可能先打印2,具体先调度子线程输出还是先调度主线程输出由系统决定。
至于为什么多次试验都是 21的结果:子线程位于主线程中,当t.start时主线程已经在运行,所以往往都先跑主线程才看到子线程结果
public class Thread_2533 {
public static void main(String[] args) throws InterruptedException {
// 记录开始时间
long start = System.currentTimeMillis();
// 1. 给定一个很长的数组 (长度 1000w), 通过随机数的方式生成 1-100 之间的整数.
int total = 1000_0000;
int [] arr = new int[total];
// 构造随机数,填充数组
Random random = new Random();
for (int i = 0; i < total; i++) {
int num = random.nextInt(100) + 1;
arr[i] = num;
}
// 2. 实现代码, 能够创建两个线程, 对这个数组的所有元素求和.
// 3. 其中线程1 计算偶数下标元素的和, 线程2 计算奇数下标元素的和.
// 实例化操作类
SumOperator operator = new SumOperator();
// 定义具体的执行线程
Thread t1 = new Thread(() -> {
// 遍历数组,累加偶数下标
for (int i = 0; i < total; i += 2) {
operator.addEvenSum(arr[i]);
}
});
Thread t2 = new Thread(() -> {
// 遍历数组,累加奇数下标
for (int i = 1; i < total; i += 2) {
operator.addOddSum(arr[i]);
}
});
// 启动线程
t1.start();
t2.start();
// 等待线程结束
t1.join();
t2.join();
// 记录结束时间
long end = System.currentTimeMillis();
// 结果
System.out.println("结算结果为 = " + operator.result());
System.out.println("总耗时 " + (end - start) + "ms.");
}
}
// 累加操作用这个类来完成
class SumOperator {
long evenSum;
long oddSum;
public void addEvenSum (int num) {
evenSum += num;
}
public void addOddSum (int num) {
oddSum += num;
}
public long result() {
System.out.println("偶数和:" + evenSum);
System.out.println("奇数和:" + oddSum);
return evenSum + oddSum;
}
}
作用功能不同:
run方法的作用是描述线程具体要执行的任务;
start方法的作用是真正的去申请系统线程
运行结果不同:
run方法是一个类中的普通方法,主动调用和调用普通方法一样,会顺序执行一次;
start调用方法后, start方法内部会调用Java 本地方法(封装了对系统底层的调用)真正的启动线程,并执行run方法中的代码,run 方法执行完成后线程进入销毁阶段。
name可以重复ID不可以重复
线程中断是线程间通信的一种重要方式,Java中线程的启动和终止,中断Java程序员说了不算,都是系统调度的,我们所谓的中断线程只是更改线程的状态而已,要想让线程终止,run方法执行结束,自然就终止了
sleep()方法属于静态方法,在哪个线程中使用,就在哪个线程中生效
调用静态方法查看线程是否中断:Thread.interrupted()
中断状态会被自动清除,会从true改为false
下图中使用 成员方法判断线程是否中断:Thread.currentThread().interrupted()
它的作用是当线程被中断时threa.interrupt()之后,不改变中断状态,仅仅只是查看当前线程是否中断,不做修改
1.当一个线程对象调用interrupt方法后(线程变为中断状态true),用类方法Thread.interrupted判断线程,会将true改为false,但是调用成员方法isInterrupted就只是查看查看线程状态,不会修改。
2.try…catch只要捕获到中断异常,无论使用哪个判断方法(静态、成员)都会使线程中断状态从
true改为false
等待一个线程也是一种线程间通信方式
加了join()方法相当于图中t1,t2,主线程三个线程变成了串行,而不是并行
t1.start();
// 主线程死等t1,直到t1执行结束主线程再恢复执行
t1.join();
// 此时走到这里,t1线程已经执行结束,再启动t2线程
t2.start();
// main -> 调用t2.join() 阻塞主线程,直到t2完全执行结束再恢复主线程的执行
// 主线程只等t2 2000ms - 2s,若t2在2s之内还没结束,主线程就会恢复执行
t2.join(2000);
// t2线程也执行结束了,继续执行主线程
System.out.println("开始学习JavaEE");
System.out.println(Thread.currentThread().getName());
}
}
1.New状态到Runnable状态只需要start()方法,start只是申请可以调度线程,并不能立即执行线程
2.Runnable就两个状态:一个Ready和Running,可执行状态并不一定真正在执行中ing
3.调用join()、sleep()方法都会把线程置为超时状态
4.超时等待时间到了就会还原状态:还原成ready状态,等待被系统调度
调用yield()方法的线程会从运行态转换为就绪态,主动让出CPU资源,什么时候让出CPU以及什么时候再次被CPU调度都是OS决定!!!
public class YieldTest {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (true) {
System.out.println(Thread.currentThread().getName());
// 春鹏线程就会让出CPU,进入就绪态,等待被CPU继续调度
Thread.yield();
}
},"春鹏线程");
t1.start();
Thread t2 = new Thread(() -> {
while (true) {
System.out.println(Thread.currentThread().getName());
}
},"云鹏线程");
t2.start();
}
}
线程不安全:在多线程场景下,串行执行和并行执行的结果不一致,就称为线程不安全。实际运行结果与我们预期不符。
比如t1和t2各自两个线程的run方法是累加5w的数值,那么最终两个线程应该相加得到10w,但是图中并发执行t1和t2线程却每次得到结果都不同。
JMM内存模型和JVM内存区域划分不是一个概念,JMM只是一个概念模型并不是真正存在的,而JVM内存区域划分是实际存在的
JMM:描述线程的工作内存和主内存
JMM内存模型:
方法中的局部变量不是共享变量
一段代码要保证线程安全问题就要满足三大特性:原子性,可见性,防止指令重排
int a=10就是一个原子性操作,要么没赋值,要么赋值成功
a+=10就是非原子性操作:先读取a的值,再执行a+10,最后将结果赋值给a,这里面包含三个原子性操作
对于共享变量的修改可以及时的被其他线程看到就叫可见性
比如图中:Counter类中的成员变量count就是共享变量,保存在堆上。可以被t1和t2线程同时访问,也就是说count这个变量保存在主内存中
此时执行increase方法可见性、原子性都不可以被保证:
count++操作:不是一个原子性操作,某个线程执行count++(原子性)这个操作时,其它线程是无法读取++后的值(可见性)
举个例子:演示不可见性,但不一定这个数值一定是图中所说的情况造成的,只是其中的一种可能性。
最后t1先执行完毕,将自己工作内存中count=5w写回主内存中对t2线程是不可见的,于是t2就会把自己工作内存中count=55659写回工作内存并将t1写回主内存的值覆盖
有很多种可能性,这里只是列举一种,总之最后的答案一定不是10w。
计划+2次,但最终只+了1次,本来t1已经将count=1写回了主内存,但是t2也让count=1写回了主内存,最后count的值还是1,正确操作之后应该是主内存中count=2才对。
类似火车的售票系统:
客户A在买票时,发现还有一张票,当他下订单时,在主内存中nums=1-1=0没票了,此时恰好客户B和A同时准备购买票,但是此时nums=0还没有写回主内存,导致B也买了这张票(超卖现象)。
从硬件角度来说:所谓的 “工作内存”, 则是指 CPU 的寄存器和高速缓存,比如执行a+10这个操作,第一步先从主内存中读取a变量的值,但是读取的值需要一个临时存储的地方,这个地方就叫寄存器。所谓"主内存"在硬件角度是真实存在的
CPU 访问自身寄存器的速度以及高速缓存的速度, 远远超过访问内存的速度(快了 3 - 4 个数量级,也就是几千倍,上万倍)
内存设备之所以这么多,还是出于成本考虑~
如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次前台。这种叫做指令重排序
此时t1线程实例化对象还没有将name成员变量赋值,但是t2线程调用per.getName()方法,于是就出现name=null的情况
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时,其他线程如果也执行到同一个对象 synchronized 就会阻塞等待
synchronized的底层是使用操作系统的mutex lock实现的
互斥:多个线程想要获取同一个对象的锁(同一个对象很关键),同一时间只有一个线程可以获取到这个锁,其它线程进入阻塞状态等待
不管几个线程对这个对象进行处理,同一时刻只有一个线程拿到这个对象的锁,拿到这个锁就会执行increase()方法,另外线程就处于等待阻塞状态,哪个线程先拿到对象的锁不确定。
每个对象都有一块描述"锁"信息的内存,这个信息告诉其它线程当前该对象有没有被哪个线程持有
当某个对象被某个线程上锁时,其它对象想要获取当前对象锁就会进入一个阻塞队列,但这个队列不遵循FIFO队列先入先出的规矩
获取锁到释放锁的中间过程保证了原子性和可见性,当其它线程再来获取锁时,保证获取到的共享变量的值一定是正确的值
同一个类中,可能存在很多方法,但是只有加上关键字Synchronized的方法它是互斥的,同一时间只能被一个线程操作(类似单线程),其它没有加关键字的方法仍然是并发执行!单线程是所有方法只有一个线程在运行
可重入:获取到"锁"的线程可以再次获取锁的操作
类似上了两次"锁"
public class Reentrant {
private class Counter{
int val;
//锁的是当前Counter对象
synchronized void increase(){
val++;
}
//还是锁的当前Counter对象
void increaseDouble(){
//在方法内部再次调用increase方法
increase();
}
}
}
如果increaseDouble()方法不上锁的情况:线程1就会阻塞在这里,等待自己释放锁之后才能再次进入increaseDouble()方法中的increase()方法,那么程序永远不会停止,线程1一直阻塞在这个位置,我们把这种情况叫做死锁!
如果某个线程加锁的时候,发现锁已经被人占用,但是恰好占用的正是自己,那么仍然可以继续获取到锁, 并让计数器自增,解锁的时候计数器递减为 0 的时候,且持有线程为null才真正释放锁(才能被别的线程获取到)
关于加两次锁的原因:只要进入一个synchronized代码块一次,就得上锁一次,计数器加一
至于加不加synchronized的问题:synchronized只是保证线程安全,对于代码编译不会产生任何问题,加不加看实际应用需求,比如你只是进行读操作,完全没有必要加synchronized。只有当修改共享变量,程序员为了保证线程安全会人为在代码中自己添加关键字。
在成员方法上加synchronized关键字,锁的就是该类的实例化对象
counter1和counter2是两个是两个不同的对象,在t1线程中counter1对象调用同步代码块的方法上锁的对象是counter1对象,t2线程中同理,获取的是counter2对象的锁
互斥现象:t1和t2线程使用同一个对象调用同步代码块的成员方法。哪个线程先获取对象锁就先执行哪个线程
锁的是一个类,不管这个类实例化多少个对象,同一时刻也只能一个线程访问
虽然看似不同对象调用静态代码块的方法,但是同一时刻只能有一个线程能获取这个锁
同步代码块之前的代码都可以并发执行,this相当于锁的是当前对象。三个线程中都是同一个对象调用同一个方法,所以在同一时刻只能有一个线程能进入这个方法
当不同的对象调用increase4()方法就不构成互斥了,因为此时每个线程中是不同的对象在调用同步代码块
同步代码块的意义就在于锁的粒度更细,方法中的其他代码仍然是多线程并发执行,如果锁的内容太多,多线程效率不高
上述锁Counter.class对象锁的是Counter的同一个实例化对象,只有是Counter类同一个对象才会上锁
下图中锁的是Reentrant主类.class这个对象,class对象全局唯一,不管是Counter的哪个对象都构成互斥,同一个时刻只能一个线程进入
ConcurrentHashMap和CopyOnWriteArrayList都属于java.util.concurrent并发工具包下的
读:直接从主内存中读取共享变量,无论当前工作内存中是否有该值
写:工作内存修改后的共享变量会立即刷新到主内存中,并且其它正在读取主内存的线程会等待,直到写操作结束
直接访问工作内存(实际是CPU的寄存器或者 CPU 的缓存),速度非常快, 但是可能出现数据不一致的情况,加上volatile,强制读写内存,速度是慢了,但是数据变的更准确了
代码示例:flag变量在没有volatile关键字修饰时,t1线程中一直处于死循环,因为 t1 线程一直读取自己的工作内存 flag=0 , t2 线程中即使改变了 flag 的值(对于t1线程来说是不可见的),t1线程也一直处于死循环,没有及时读取主内存中更新后的flag值。
但是在共享变量 flag 之前加上关键字 volatile ,线程 t1 会立即退出循环,因为 volatile 保证可见性,保证 t1 线程每次强制读取主内存中 flag 的数据
synchronized 同样可以保证可见性,去掉 flag 前的关键字volatile,给counter对象上锁,在线程 t2 中修改 flag 的值同样可以立即终止线程 t1
volatile 和 synchronized 有着本质的区别:synchronized 能够保证原子性,volatile 保证的是内存可见性不保证原子性,比如在之前写的t1线程累加5w和t2线程累加5w的例子中,即使给共享变量加上关键字volatile,同样得不到正确答案10w
java语言是无法保证原子性的,要保证原子性只能采取上锁等方式,保证哦同一时间只有一个线程操作就能让不原子性操作变得原子性
一定保证共享变量的可见性的关键字就是synchronized关键字、volatile关键字、final关键字
此外final可以保证可见性原因:final关键字修饰的变量为常量,必须在初始化时赋值且不能更改,所以保证了天然的可见性
等待和唤醒也是一种线程间通信的方式,Object类方法表示任意实例化对象都具有wait()和notify()方法
使用wait()方法会释放锁,线程会进入等待队列
带有时间参数timeout的wait方法,时间参数单位是ms
1.方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
2.如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)
3.在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
注意事项:
1.如果没有搭配synchronized使用notify和wait方法会报错
2.唤醒等待线程也需要使用同一个对象的notify方法
调用notify方法唤醒在等待的线程,一定要等notify方法中同步代码块里执行完毕才可以执行被唤醒的线程。
修改 NotifyTask 中的 run 方法,,把 notify 替换成 notifyAll:
虽然是同时唤醒 3 个线程,,但是这 3 个线程需要竞争锁,所以并不是同时执行
例如图中t3先获取到锁,所以t1和t2首先进入阻塞队列,t3调用wait方法就释放对象锁,t1和t2就去竞争这个对象锁,t3就会进入等待队列。当t1 t2 t3都进入等待队列,调用notifyall方法,则三个线程都被唤醒,但不是立即并发执行这三个线程,从等待队列同时将三个队列置入阻塞队列去竞争锁,不遵循先来先获取锁的原则
相同点:其实本质上两者并没有什么联系,但是可以同时让线程放弃执行一段时间
不同点:
sleep():线程Thread类提供的方法,不需要搭配关键字,并且调用方法时不需要释放锁
wait():Object类提供的方法,必须搭配synchronized关键字使用,调用方法会释放锁
设计模式:软件开发中也有很多常见的 “问题场景”,针对这些问题场景,大佬们总结出了一些固定的套路
不管外部调用或者不调用这个对象,只要类加载到JVM,唯一对象就会产生
构造方法私有化,可以保证对象产生的数量
单例对象使用静态变量static的原因:在类的内部只用一次构造方法。外部访问成员变量需要通过对象,但是外部没有这个对象,此时外部想要获取这个唯一对象就需要把这个对象设置为静态变量,获取的方式:通过get方法
这个类只要一加载就产生唯一对象(饥不择食),不管外部是否需要这个对象,只要类加载到JVM,唯一对象就产生
/**
* 饿汉式单例模式
* @author hide_on_bush
* @date 2022/7/18
*/
public class SingleTon {
//只产生一个对象
private static SingleTon singleTon=new SingleTon();
//私有化构造方法
private SingleTon(){
}
//get方法返回唯一对象
public static SingleTon getSingleTon() {
return singleTon;
}
}
无论是用多少个引用都是同一个对象,并且构造方法私有化,无法通过new产生实例化对象
懒汉式单例更常见的原因:按需分配,只有需要实例化才产生对象,节省内存空间
hashMap产生对象时都没有初始化table数组,只有第一次调用put方法才初始化数组大小,节省空间
饿汉式是天然的线程安全,JVM加载类时就创建了这个唯一对象;但是懒汉式就不一样,假设同时三个线程并行执行调用get方法,会发现对象为null,此时可能三个线程都会同时创建三个不同的对象
1.直接在方法上加锁:效率不高,锁的粒度太粗
2.优化 double - check
此时假设三个线程同时执行到同步代码块,当t1获取到这个锁进入同步代码块创建对象后释放锁,t2和t3还是会从开始时竞争锁的位置开始执行,还是会再次创建两个不同的对象,所以不可行
1.有三个线程,开始执行get方法,通过外层的 if (singleTon = null) 知道了实例还没
有创建的消息,于是三个线程开始竞争同一把锁
2.其中线程1率先获取到锁,此时线程1通过里层的 if (single= null) 进一步确认实例是
否已经创建,如果没创建,就把这个实例创建出来
3. 当线程1 释放锁之后,线程2 和线程3也拿到锁,也通过里层的 if (instance == null) 来
确认实例是否已经创建,发现实例已经创建出来了,就不再创建了
4.后续的线程,不必加锁,直接就通过外层 if (instance = null) 就知道实例已经创建了,从
而不再尝试获取锁了,降低了开销
3.使用volatile关键字
加锁 / 解锁是一件开销比较高的事情,而懒汉模式的线程不安全只是发生在首次创建实例的时候, 因此后续使用的时候,不必再进行加锁了,外层的 if 就是判定下看当前是否已经把 instance 实例创建出来了
volatile保证防止指令重排
加volatile关键字原因:假设此时程序中有两个线程,t1先获取锁执行同步代码块,t2刚开始执行会卡在第一个if语句,但是t1执行同步代码块会产生一个对象,那么此时t2会看到singleTon这个对象不等于null,可能会直接return 这个唯一对象,但是t1线程初始化对象还没有结束,返回的可能是一个不完整的对象,有了volatile关键字修饰才能保证JVM执行完new操作再返回对象
/**
* 生产者 - 消费者模型
* @author hide_on_bush
* @date 2022/9/20
*/
public class Consumer_Producer {
public static void main(String[] args) {
//阻塞队列
BlockingQueue<Integer> blockingQueue=new LinkedBlockingQueue<>();
Thread consumer=new Thread(()->{
try {
int val=blockingQueue.take();
System.out.println("消费元素:"+val);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"消费者");
Random random=new Random();
Thread producer=new Thread(()->{
try {
int val= random.nextInt(100);
blockingQueue.put(val);
System.out.println("生产者放入一个元素:"+val);
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"生产者");
consumer.start();
producer.start();
}
}
标准库中提供了一个 Timer 类,Timer 类的核心方法为 schedule
schedule 包含两个参数:第一个参数指定即将要执行的任务代码,是一个new TimerTask任务,就是Runnable接口的子类;第二个参数指定多长时间之后执行 (单位为毫秒)
corePoolSize:核心池线程数量(正式工)
maximumPoolSize:线程池最大线程数量(正式工+临时工)
keepAlive:线程池临时线程允许空闲时间
workQueue:工作队列,实质上就是个阻塞队列,线程从中取出执行任务
hander:拒绝策略,当任务数量超出线程池负荷时怎么办
固定线程池:没有临时工,最大线程数量==核心池线程数量
动态缓存池:核心线程为0,每当有新任务进来都是临时创建线程
工作队列(阻塞队列)几乎用不上,最大线程数能达到40多亿
单线程池:只有一个线程,所以需要无解界限的工作队列
固定大小延迟线程池:
单独创建一个线程:执行完一次任务就需要销毁
单线程池:将任务不断提交到阻塞队列中,线程只需要不断调度工作队列中的任务即可
调用submit()方法提交一个线程任务
判断当前任务数量是否大于核心池线程数量
若小于:无论当前是否有空闲线程都会创建一个新线程执行任务,而后将该线程保存到corePoolSize(招聘一个正式工)
若大于:会再次判断工作队列中是否已满
判断工作队列是否已满时:
工作队列未满:将任务入队,等待线程调度
工作队列已满:判断当前线程池数量maximumSize是否已经达到最大值(正式工+临时工)
乐观锁的一个重要功能就是要检测出数据是否发生访问冲突. 我们可以引入一个 “版本号” 来解决.
乐观锁的实现案例:设当前余额为 100. 引入一个版本号 version,初始值为 1 并且我们规定 “提交版本必须大于记录当前版本才能执行更新余额”
线程 1 在自己的工作内存中减去50(100-50),线程 2 在自己的工作内存中减去20(100-20)
版本号:记录更新的次数。当线程1先将50写回主内存中时,版本号+1,表示更新了一次主内存。
线程 2 也想将自己工作内存的80写回主内存,但此时发现主内存版本号等于2,线程 2 自己的版本号等于1,两者版本号不相等,无法将80写回主内存,写入失败。不满足 “提交版本必须大于记录当前版本才能执行更新” 的乐观锁策略。就认为这次操作失败
解决策略:
1.直接报错
2.CAS策略:线程2先读取主内存版本号为2,再将主内存的新数据写回自己的工作内存进行操作(减20操作),最后将30写回主内存,并且版本号+1等于3
数据的读取一般不会发生线程安全问题,只有更新数据(CURD)的时候才可能发生线程安全问题。
JDK内置的读写锁:ReentrantReadWriteLock
读加锁和读加锁之间,不互斥
写加锁和写加锁之间,互斥
读加锁和写加锁之间,互斥
轻量级锁采用的自旋锁:获取线程失败不阻塞,不让出CPU。如果获取锁失败, 立即再尝试获取锁,无限循环,直到获取到锁为止,第一次获取锁失败, 第二次的尝试会在极短的时间内到来,一旦锁被其他线程释放,就能第一时间获取到锁
Synchronized锁就是典型的非公平锁
操作系统内部的线程调度就可以视为是随机的,如果不做任何额外的限制, 锁就是非公平锁,如果要想实现公平锁,就需要依赖额外的数据结构,来记录线程们的先后顺序
1、线程中的start()方法和run()方法的主要区别在于,当程序调用start()方法,将会创建一个新线程去执行run()方法中的代码。但是如果直接调用run()方法的话,会直接在当前线程中执行run()中的代码,注意,这里不会创建新线程。这样run()就像一个普通方法一样。
2、另外当一个线程启动之后,不能重复调用start(),否则会报IllegalStateException异常。但是可以重复调用run()方法。