目录
①直接加到一个普通的方法上,进入方法就相当于加锁,出了方法就相当于解锁。(锁对象还是this)
③加到一个static方法上,此时相当于指定了当前的类对象为锁对象。
八、JMM(Java Memory Model,Java存储模型/Java内存模型)
注意2:如果有多个线程都在等待,调用一次notify,只能唤醒其中的一个线程,具体唤醒的是谁,随机的。
注意3:如果没有任何线程等待,直接调用notify,不会有副作用。
一、进程(process/task)
1.1 进程的概念
一个跑起来的程序,就称为进程。进程也可以认为是一个可执行文件跑起来之后动态的过程。
进程运行后会被系统分配一些资源。
进程是系统资源分配的基本单位。
1.2 进程控制块对象PCB
1.2.1 操作系统如何管理进程?
操作系统要保证这么多进程,每个都能正常的进行工作,而且互相之间并不影响。他的管理分为两个部分,即:管理=描述+组织。
描述:使用PCB(process control block)这样的结构体来表示一个进程的相关属性。
组织:使用一定的数据结构把正在执行的进程给串起来。(在Linux中,使用的是双向链表,windows并不开源,所以不清楚)
当我们双击一个exe运行一个进程的时候,操作系统内核就会创建PCB,并把这个PCB加到双向链表中。
当关掉一个程序时,操作系统就会找到对应的PCB,并且从链表上删掉。像任务管理器,能够显示出当前所有的进程信息,就是在遍历这个链表。
(这样写有点不准确,后文会再重新描述一下)
1.2.2 PCB里面的内容
pid:进程的身份标识。
内存指针:表示进程的代码段和数据段都在哪里。exe文件存储在硬盘上,双击运行,操作系统就会把这个硬盘上的数据加载到内存中,内存中就包含了这个程序对应的指令是啥(代码段),依赖的数据是啥(数据段)。内存指针就描述了哪里是代码段,哪里是数据段。
文件描述符表:进程打开了哪些文件。文件描述符表可以视为一个数组,数组的每个元素,是一个特殊的结构体,就描述了一个文件的信息。这个数组的下标称为文件描述符。每个进程一启动,默认就会打开三个文件,即在文件描述符表中创建三个项,分别是标准输入(System.in)、标准输出(System.out)、标准错误(System.err)。如果通过代码再打开其他文件,同样也会再文件描述符表中创建出对应的项。
状态:描述了当前这个进程,是否能去CPU上执行。如,就绪状态、睡眠状态
上下文:操作系统在执行某个进程时,如果需要把这个进程从CPU上调度走,就需要保存CPU的运行现场(当前寄存器里面的数值都是啥)到内存中,下次再调度到这个进程的时候,就可以无缝的继续从上次的位置往后执行。
优先级:调度进程的时候,每个进程安排的时间和先后都可以存在差别。
记账信息:统计每个进程执行的时间和指令的数目,依据这个来平衡调度的效果。
1.3 进程调度
操作系统,需要对上述的进程进行调度。
并行:2个核心,同时执行两个进程的指令。
并发:1个核心,“同时”在执行多个进程的指令。是靠快速的切换。
1.9GHZ:每分钟能执行19亿条指令。
现代的操作系统,都是支持这种多任务的操作系统,多个任务都是通过并行+并发的方式来调度执行的。一般使用并发来代指并行+并发
1.4 进程的独立性
操作系统要给软件提供一个稳定的运行环境,意思就是,系统要能够保证一个进程出问题不会影响到其他的进程,更不会波及到整个系统。
操作系统给每个进程分配一个独立的“虚拟内存空间”,不同进程访问的内存没有公共区域,就能保证互相之间不产生影响。
1.5 进程间通信机制
进程之间存在独立性,但有时需要进程之间的相互配合完成一部分工作。操作系统提供了一些“进程间通信”的机制,就可以完成一些沟通交互工作。
进程间通信机制,就是专门提供了一些区域,可以让多个进程可以同时访问到(共享)。
操作系统提供的进程间通信的机制有多种,主要学习操作文件和操作网络这两种。
1.6 进程的缺点
引入进程,就是为了解决并发编程的问题。但引入了一些其他问题。
进程持有的一些系统资源比较多,创建进程需要分配资源,销毁进程需要释放资源。进程调度切换,也需要让这些资源之间进行切换。这些操作都是比较耗时间的。如果切换的频率比较频繁,这时我们的成本是比较高的。
1.7 解决方案
进程池,类似于字符串常量池
通过线程来解决问题。线程也称为轻量级进程,LWP(light weight process).
二、线程概念
2.1 概念
一个线程就是一个“执行流”,每个线程执行着自己的代码。同一个进程中的若干线程,共用同一个内存空间。(进程就像是一个工厂,线程就是工厂里面的生产线)
为什么要实现并发编程?
①单核CPU的发展遇到了瓶颈,要想提高运算力,就需要多核CPU。并发编程能更充分的利用多核CPU的资源。
有些任务场景需要等待“IO”,为了让等待IO的时间能够去做一些其他的工作,也需要用到并发编程。
多线程实现并发编程的优势:
由于资源是绑定在进程上的,创建销毁线程和进程上的资源关系不大。创建线程,并不需要分配新的资源,释放线程也不需要释放旧的资源。CPU针对线程进行调度,开销也是小于进程。总结为:
创建线程比创建进程更快、销毁线程比销毁进程更快、调度线程比调度进程更快
2.2 进程和线程的区别
①进程包含线程,一个进程里有一个线程,也可以有多个线程。
②进程存在的意义就是为了解决并发编程的问题,如果频繁创建或者销毁进程,开销比较大。相比之下线程也能满足并发编程的需求,但是线程的创建和销毁开销就小很多。
③进程是系统分配资源的基本单位,线程是系统调度执行的基本单位。
④进程之间各自有各自的虚拟地址空间,一个进程崩溃了不会影响其他进程。但是同一个进程里的线程共用一个虚拟地址空间,如果一个线程挂了,很容易影响到其他线程,甚至把整个进程都搞崩溃。
2.3 线程出现的问题
如果对线程的创建和销毁的频率很高,线程也会显得比较重量了。有两种解决方案:
线程池、协程(纤程)
三、创建线程
在Java标准库中,通过Thread类来表示线程。创建的每个Thread实例都是和系统中的一个线程是对应的。
在实例化时,可以传入一个name。通过这个给线程命名,那么对于代码执行没有影响。
如果不指定name,JVM默认指定的名字就是类似于Thread-0,Thread-1等。
3.1 继承Thread类
创建一个子类,继承Thread。重写Thread的run方法。
run方法里面包含了这个线程要执行的代码。即,当线程跑起来了,就会依次来执行run方法中的代码。
class MyThread extends Thread{
@Override
public void run() { // 在新的线程里就会执行这个方法
while (true){
System.out.println("hello thread");
try {
Thread.sleep(1000); // 称为休眠。在这个过程中,这个线程就不会占用CPU了
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Demo1 {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start(); // 会在系统中创建一个新的线程
while (true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
一个进程里面至少有一个线程。main方法也是通过一个线程来执行的。
以往写的代码的每个main方法,都对应了一个线程。
上面的代码通过 myThread.start();又创建了一个新的线程。
运行结果:
从运行结果中可以看到,这两个线程的打印是交替进行的。不是打印完一个,再打印另一个,这就意味着这两个线程是并发执行的关系(在宏观上同时执行)。
同时,这里的并发,也不是严格意义上的你一条我一条,偶尔也会出现你一条我两条、我两条你一条的情况。
多个线程执行的先后顺序,并不是完全确定。当1s时间到了后,系统先唤起哪个线程是不确定的,取决于操作系统内部调度代码的具体实现。
如果多个线程之间没有手动的控制执行先后顺序,这个时候就认为多个线程之间的执行是“随机顺序”的。这也是多线程编程的万恶之源。
jconsole工具
jconsole工具相当于一个监控程序,能够看到一个Java进程内部很多的详细信息。
在哪找:
主要是看jdk安装在哪。我的就是:C:\Program Files\Java\jdk1.8.0_192\bin
3.2 实现Runnable接口
①创建一个类,实现Runnable接口(标准库自带的),也是重写run方法。
②创建Thread实例,将刚才的Runnable实例给设置进去。
class MyRunnable implements Runnable{
@Override
public void run() {
while (true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 这种是通过实现Runnable接口实现的,通过这种方式,就相当于把 要执行的任务 和 Thread类,进行分离(解耦合)
public class Demo1 {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread t = new Thread(myRunnable);
t.start();
while (true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
3.3 使用匿名内部类
public class Demo1 {
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run() {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t.start();
while (true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Demo1 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while (true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t.start();
while (true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
3.4 使用lambda表达式
public class Demo1 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
while (true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
3.5 感受多线程的优势(增加运行速度)
未使用多线程:
// 串行执行
public static void serial(){
long beggin = System.currentTimeMillis();
long a = 0;
for (int i = 0;i<10_0000_0000;i++){
a++;
}
long b = 0;
for (int i = 0;i<10_0000_0000;i++){
b++;
}
long end = System.currentTimeMillis();
System.out.println(end-beggin);
}
public static void main(String[] args) {
serial();
}
引入多线程:
public static void main(String[] args) {
conCurrency();
}
public static void conCurrency(){
long beggin = System.currentTimeMillis();
Thread t1 = new Thread(() -> {
long a = 0;
for (int i = 0;i<10_0000_0000;i++){
a++;
}
});
t1.start();
Thread t2 = new Thread(() -> {
long b = 0;
for (int i = 0;i<10_0000_0000;i++){
b++;
}
});
t2.start();
// 不能直接记录结束时间
// 因为conCurrency和t1、t2是并发关系,t1和t2还没执行完呢,就直接记录结束时间不准确
// 要记录两个线程执行的最慢的时间,作为结束时间
// 引入join,就是等待线程结束t1.join 意思就是等到t1执行完了,才会返回,继续往下走
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println(end-beggin);
}
CPU密集型:程序里面要进行大量的运算。
并发编程的最明显的优势,就是针对“CPU密集型”的程序,能够提高效率。
四、Thread类的常见属性和常用方法
4.1 Thread的几个常见属性
属性 | 获取方法 |
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台程序 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
补充:如果创建的线程是一个非后台的线程,即使main方法执行完了,java进程仍需要继续执行,直到所有的非后台程序都执行完,Java进程才会退出。
如果创建的是后台线程,不会影响Java进程的结束。
4.1. run方法和start方法的区别
start:创建新线程
run:执行线程入口逻辑的方法。本身不具备创建新线程的作用。
线程被创建了,内核里不一定有对应的线程,start方法调用后,才会有对应的线程。
假设不调用start(),调用run(),会发生什么呢?
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run() {
while (true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t.run();
while (true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
只打印了hello thread,没有打印hello main。当前的run只是一个普通方法的调用,并没有创建出新的线程。当前代码只有main这一个线程,当run方法里面的循环执行完了,才能执行后续的代码。但run方法里面的循环是一个死循环。
4.2 中断新线程
①自定义一个标志位
public class Demo2 {
public static boolean isInterrupt = false;
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (!isInterrupt){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
System.out.println("线程还没有结束");
t.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
isInterrupt = true;
System.out.println("线程已经结束啦");
}
}
isInterrupt变量相当于在t线程中读取,在main方法中修改。这样写会存在一些问题,见6.2中的④内存可见性。
②采用Thread提供的标志位
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run() {
// isInterrupted()是线程自带的标志位,通过这个判断标志位是否为true,为true表示线程应该退出
while(!this.isInterrupted()){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
System.out.println("线程还没有结束");
t.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 休眠5s后,来控制t线程终止
t.interrupt();
System.out.println("线程已经结束啦");
}
从运行结果可以看出,当触发中断方法的时候,t线程就只是输出日志,就继续往下执行了,并没有真的停下来。
主要原因是:在catch里面不作为。
在t的线程代码中,存在两种情况:
①执行打印和while循环打印(程序处于就绪状态)
②进行sleep,处于阻塞状态/休眠状态
调用interrupt方法的时候:
进程处于就绪状态:此时直接修改线程中对应的标志位
进程处于阻塞状态:此时就会引起InterruptedException。
出现了异常,并没有对这个异常进行实质性的处理,相当于把这个异常给忽略了。加入对异常的处理:
运行结果:
interrupt这个方法,说是中断线程,但是并不是直接就立刻马上的杀死线程,具体线程怎么退出,得线程代码自己说了算。
线程的代码处理interrupt方法有三种方式
第一种:立刻结束
第二种:执行一些操作后再结束。
第三种:忽略。
为什么不将interrupt设计为一调用就结束线程?
以前这样设计过。由于t线程和main线程是并发执行的关系,当main执行interrupt时,并不知道t执行到哪,冒然中断会导致一些问题。线程t 受到interrupt时,怎么处理由t自身来决定,就可以保证t把任务干完,将收尾工作做好,然后再被销毁。
4.3 等待一个线程(join)
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run() {
for(int i = 0;i<5;i++){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
System.out.println("线程还没有结束");
t.start();
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程已经结束啦");
}
}
在main的线程中调用t.join(),就是让main来等待t执行完毕。
当main中的代码执行到t.join()时,main线程就会进入阻塞等待状态。效果和sleep类似。等t线程执行结束了,即t中的run方法执行完了,main线程就会继续执行。
通过join可以控制线程的结束顺序。
join不带参数,就是"死等"。
join带参数:就是表示等待的最大时间。一般采取这种方式。
4.4 获取当前线程引用
在某个线程的代码中,拿到这个线程对应的thread对象的引用,才能进行后续的一些操作。很多和线程相关的操作,都是依赖这样的引用。
①如果直接继承Thread创建的线程,直接在run方法中调用this就可以拿到这个线程的实例。
在这个代码中,通过this.isInterrupted()是不能调用的,因为run是Runnable的方法,不是Thread里的方法。此处的this没有指向Thread,当然就没有Thread类的属性和方法。
②更常见的是使用Thread里面的一个静态方法,currentThread(),哪个线程调用了这个静态方法,就能够返回哪个线程的Thread实例引用。
虽然run仍然是Runnable的方法,但是通过这个Thread的currentThread来获取线程实例。
即在哪个线程里调用Thread.currentThread(),就返回哪个线程的实例。
4.5 休眠当前线程
调用sleep的线程会阻塞等待,等待时间取决于设置的时间。
在大多数情况,一个进程中有多个线程,每个线程对应着一个PCB,一个进程对应了一组PCB。
操作系统是以PCB为单位进行调度的,所以线程是操作系统调度执行的基本单位。
五、线程的状态
NEW:把Thread对象创建出来了,但是内核里面的线程还没创建。(没有调用start方法)
TERMINATED:内核里的线程已经结束了,但是Thread对象还在。(线程执行完run方法以后)
RUNNABLE:就绪状态(随时可以被调度到CPU中运行)
TIMED_WAITING:阻塞状态,一定时间后移出阻塞。通过sleep产生
BLOCKED:阻塞状态,等待锁的时候产生的。
WAITING:阻塞状态,调用wait产生的。
六、线程安全问题(重点哦)
6.1 经典线程不安全示例
class AddSum{
public static long sum = 0;
public static void increase(){
sum++;
}
}
public class Demo11 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for(int i = 0;i<5_0000;i++){
AddSum.increase();
}
});
Thread t2 = new Thread(() -> {
for(int i = 0;i<5_0000;i++){
AddSum.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(AddSum.sum);
}
}
运行结果:每运行一次,得到的结果都不同。结果在5万到10万之间。这个bug是因为线程调度的随机性导致的。
为什么呢???在了解前,先认识什么是线程 不安全。
编写的多线程代码,如果当前代码因为多线程随机的调度顺序,导致程序出现了bug,就称为线程不安全。如果不管系统按照啥样的随机情况来调度,也不会导致系统出现bug,就称为线程安全。
这里的网络不安全和黑客没有关系。
看看sum++都干了啥?
sum++操作其实是三个步骤:
step1:把内存的值,读到CPU的寄存器中 load
step2:把寄存器中的0进行+1操作 add
step3:把寄存器中的1给写回到内存中 save
这三个步骤,对应三个指令
如果两个线程同时操作这个sum,此时由于线程之间的随机调度的过程,可能产生不同的结果。
如何让线程安全,常用方案是加锁synchronized(Java中内置的关键字),进入increase方法后,就先加锁,出了这个方法就解锁。
引入多线程,目的是为了实现并发编程,当加了锁之后,数据结果是对的,但是这里的并发性就降低了,速度也慢下来了。
追求速度还是追求准确?准确
两个并发的线程,可能各自要完成的任务很多,有很多工作能够并行进行的,整体来说,多线程还是有意义的。
6.2 线程不安全的原因
①根本原因,线程的抢占式执行(这个无可奈何)
②两个线程在修改同一个变量
如果是一个线程修改这个变量,没事;
如果是两个线程读这个变量,也没事;
如果是两个线程修改两个变量,也没事。
针对这个原因,可以在一定程度上进行处理。例如修改代码结构,避免出现这种多个线程修改一个变量的情况。
③线程针对变量的修改操作不是原子性的
原子性就是不可拆分性,例如++就不具有原子性,它可以拆分为load add save三个操作
加锁操作本质上就是把这些不是原子性的操作给打包在一起了,这种做法普适性最高,也是处理线程安全最典型的办法。
④内存可见性(重点)
假设有一个变量,一个线程快速的读取这个变量的值,另一个线程会在一定时间后,修改这个变量
public class Demo2 {
private static int isQuit = 0;
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (isQuit == 0){
// 循环体里啥都不写
}
});
t.start();
// 在主线程中,通过Scanner输入一个整数,把输入的值赋值给isQuit,从而影响线程2
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个一个整数");
isQuit = scanner.nextInt();
System.out.println("main线程结束");
}
}
main线程修改了isQuit的值,但是t线程却没有随之退出。显然,这是一个bug。
Java中的编译器优化
程序员写的代码,Java编译器在编译的时候,并不是原封不动的逐字翻译。会保证在原有逻辑不变的前提下,动态调整要执行的指令内容。这个调整能够提高程序的运行效率.
这里的优化会提高程序的运行效率,但在多线程的场景下,编译器的判断可能会存在误差,优化操作后就有可能影响到原有的逻辑。
isQuit == 0,分为load和compare两个指令。在t线程中,编译器的直观感受就是“反复的进行了太多次的load,太慢了,而且,由于编译器无法将多个线程联系在一起分析,就会觉得load得到的结果好像还是一直不变的”,因此,编译器有了一个大胆的优化操作,就是直接省略这里的load,只保留了第一次。在后续的比较操作中,就不再重新读取内存了,而是直接从寄存器中读。这样,t线程少了很多的load操作,速度就会提高不少。
load是从内存中读数据,比直接从寄存器读数据慢了3-4个数量级。
解决方案:
a. 适用synchronized,就会禁止编译器在synchronized内部产生上述的优化
b. 还可以使用另一个关键字,volatile,保证了内存的可见性,禁止了编译器的相关优化
。
可以采用关键字修饰对应的变量,编译器在优化的时候,就知道会禁止上述读内存的优化,保证每次都是重新从内存中读,哪怕速度慢一点。
⑤指令重排序
也是编译器的一种优化手段,保证原有代码逻辑不变,调整了指令的执行顺序,从而提高了效率。
如果是在单线程情况下,这里的判定比较准。如果在多线程情况下,这里的判断就不一定准了。
解决方案:加synchronized。编译器对于synchronized内部的代码是非常谨慎的,不会随便优化
6.3 synchronized的具体使用用法
synchronized,起到的效果,有三个方面:
互斥(将随机执行的线程顺序变为串行执行)
保证内存可见性
禁止指令重排序。后面这两点都是在提醒编译器能够优化的谨慎一点
①直接加到一个普通的方法上,进入方法就相当于加锁,出了方法就相当于解锁。(锁对象还是this)
②加到一个代码块上,需要手动指定一个锁对象。
在Java中,任何一个继承自Object类的对象,都可以作为锁对象(synchronized加锁对象操作,本质上就是操作Object对象头中的一个标志位)
③加到一个static方法上,此时相当于指定了当前的类对象为锁对象。
两个线程针对同一个对象加锁,才会产生竞争。两个线程针对不同的对象加锁,不会产生竞争。
public class Demo2 {
private volatile static Object locker1 = new Object();
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (true){
// 获取到锁之后,就让程序阻塞,通过Scanner来进行阻塞
synchronized (locker1){
Scanner scanner = new Scanner(System.in);
System.out.println("输入一个整数");
int a = scanner.nextInt();
System.out.println(a);
}
}
});
t.start();
// 加这个sleep,保证t1先拿到锁,先执行
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread t2 = new Thread(() -> {
synchronized (locker1){
System.out.println("t2");
}
});
t2.start();
}
}
t1先运行,t2后运行。t1先拿到locker1这个锁,然后就阻塞。t2想拿到这个锁时,由于t1已经占用了锁,所以t2线程无法获取到锁,就只能阻塞等待
没有打印t2这个日志,当前这个t2是阻塞的。
如果两个线程针对不同的对象加锁,这两个线程之间就不会有任何的竞争关系。
public class Demo2 {
private volatile static Object locker1 = new Object();
private volatile static Object locker2 = new Object();
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (true){
// 获取到锁之后,就让程序阻塞,通过Scanner来进行阻塞
synchronized (locker1){
Scanner scanner = new Scanner(System.in);
System.out.println("输入一个整数");
int a = scanner.nextInt();
System.out.println(a);
}
}
});
t.start();
// 加这个sleep,保证t1先拿到锁,先执行
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread t2 = new Thread(() -> {
synchronized (locker2){
System.out.println("t2");
}
});
t2.start();
}
}
可重入锁:
是synchronized的重要特性,如果synchronized不是可重入的,就很容易出现死锁的情况。
当调用increase时,先针对this进行加锁操作(此时this就是一个锁定的状态,将this对象头中的标志位给设置上了)
继续往下执行,也会尝试再次加锁。由于此时this已经是锁定状态了,按照之前的理解,这里的加锁就会出现阻塞,阻塞会一直持续到之前的代码把锁释放掉了 ,即:要执行完整个方法,锁才能释放,但是由于此时的阻塞,导致当前的这个方法无法再继续下去。
当有可重入锁的特点后:
首先,锁中有两个信息:当前这个 锁被哪个线程给持有了、当前这个线程被加锁了几次
当线程t已经加锁成功后,后续再次尝试加锁,就会自动的判定出,当前这把锁就是t持有的,第二次加锁不会真的加锁,只是进行一个修改计数。代码接着往下执行,出了synchronized代码段,就会触发一次解锁,也不是真的解锁,而是计数-1.当外层方法执行完之后,再次-1,减为0后,才真正的进行解锁。
死锁出现的情况:
一个线程两把锁
两个线程两把锁
N个线程M把锁(哲学家就餐问题)
七、集合类的线程安全问题
线程不安全:谨慎在多线程环境下使用,尤其是一个对象被多个线程修改的时候
ArrayList、LinkedList、HashMap、TreeMap、HashSet、TreeSet、StringBuilder
线程安全:(因为其核心方法上带有了synchronized关键字)
Vector、HashTable、ConcurrentHashMap、StringBuffer
String也是线程安全的。因为String是不可变对象,因此,就不能再多个线程中修改用一个String。
synchronized和volatile的区别:
synchronized:原子性、内存可见性、指令重排序
volatile:内存可见性
八、JMM(Java Memory Model,Java存储模型/Java内存模型)
JMM其实就是前面说的CPU的寄存器以及内存之间一套模型,Java将其抽象并重新命名,称为JMM
将CPU的寄存器这部分存储,称为:工作存储/工作内存(Work Memory)
将正常的内存,称为主存储/主内存(main Memory)
九、控制线程的执行顺序-wait notify机制
当某个线程调用了wait之后,就会阻塞等待,直到其他某个线程调用notify把整个线程唤醒为止。因此,可以利用wait-notify控制线程的执行顺序。
注意1:wait需要搭配synchronized适用
wait方法里会做三件事情:
先针对o解锁
进行等待,等到通知的到来
当通知到来之后,会被唤醒,同时尝试重新获取到锁,然后再继续执行。
因此,wait需要搭配synchronized来使用
notify也是Object类的方法。哪个对象调用的wait,就需要哪个对象调用notify来唤醒。
notify同样也要调用synchronized来使用。
注意2:如果有多个线程都在等待,调用一次notify,只能唤醒其中的一个线程,具体唤醒的是谁,随机的。
注意3:如果没有任何线程等待,直接调用notify,不会有副作用。
notifyAll:以下全部唤醒,唤醒之后,这些线程再尝试竞争这同一个锁。
唤醒一个,其他线程仍然在wait中阻塞;唤醒全部,这些线程尝试竞争锁,然后按照竞争成功的顺序,依次往下执行。
private static Object locker = new Object();
public static void main(String[] args) throws InterruptedException {
Thread waiter = new Thread(() -> {
while (true) {
synchronized (locker) {
System.out.println("wait 开始");
try {
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("wait 结束");
}
}
});
waiter.start();
Thread.sleep(3000);
Thread notifier = new Thread(() -> {
synchronized (locker) {
System.out.println("notify 之前");
locker.notify();
System.out.println("notify 之后");
}
});
notifier.start();
}