JavaEE 初阶:线程(2)
等待一个线程--join()
多线程之间并发执行,随机调度,但是join()的作用可以帮助我们控制线程结束的先后顺序。
join() 的优点
虽然我们可以通过sleep休眠来控制线程结束的顺序,但是我们设置时间来进行休眠等待的时候,由于无法确定时间线程执行的时间比我们设置的时间的长和短,导致出现没有必要的等待,或者线程没有完成,休眠会指定CPU等待的时间,导致CPU时间的浪费,而join()只会使当前的线程阻塞,直到任务的完成。
public class Demo12 {
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->{
for (int i = 0; i < 5; i++) {
System.out.println("Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("结束");
});
t.start();
//意思为等到thread全部执行完,才开始执行main
t.join();
//得到一定的时间如果还没有等到thread执行完的话,就执行main
//t.join(2000);
System.out.println("main");
//Thread.sleep(3000);
}
}
我们可以观察一下线程
在main线程中t.join意思是让main线程等t线程先结束,当执行到t.join()使,main线程就会阻塞等待,一直到t线程执行完毕,join才会继续执行。
注:如果t线程一直不结束,join就会一直不执行
所以我们就要进行优化,给join加上最大的等待时间
如果2000毫秒之后,t线程还没有结束的话,那么就不进行等待开始执行main线程,(2000)就是最大的等待时间。
获得当前线程的引用
public static Thread currentThread()-获得当前线程的引用
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
}
}
休眠当前的线程
在设置休眠的过程中,我们需要保证的一点是我们设置的休眠时间,实际上是要大于参数设置的休眠时间的。
实质:代码调用sleep,相当于让出了CPU资源,等到时间到了的时候,需要操作系统内核,把线程重新调度到CPU上,才能继续执行。
sleep(0)的作用:
让出了当前的线程,放弃了CPU资源,等待操作系统重新调度。(把CPU让出,给别人更多的执行机会)
进程状态
1.就绪 2.阻塞
NEW:表示安排了工作还未开始执行。(new了Thread对象,还没start)
TERMINATED:工作完成了(内核中的线程已经结束了,但是Thread对象还在)
TIMED_WAITING:这几个都表示排队等着其他事情
- 指定时间阻塞
- 线程阻塞(不参与CPU调度,不继续执行了)
- 阻塞的时间是有上线的
public class Demo13 {
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->{
for (int i = 0; i < 5; i++) {
System.out.println("Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
System.out.println(t.getState());
t.start();
Thread.sleep(500);
System.out.println(t.getState());
}
}
RUNNABLE:可工作的,又分为正在工作中和即将开始工作。
就绪
- 线程正在CPU上执行
- 线程随时去CPU上执行
WAITING:这几个都表示排队等着其他事情
- 死等,没有超过时间的阻塞等待
多线程带来的风险-线程安全
main 在t1.join处阻塞,等t1结束,在t2.join处阻塞,等t2结束,main继续执行。
最终打印的值,使t1和t2都执行完的值。
public class Demo15 {
public static int count;
public static void main(String[] args) throws InterruptedException {
Object locker1=new Object();
Object locker2=new Object();
Thread t1=new Thread(()->{
for (int i = 0; i < 5000; i++) {
count++;
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 5000; i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
// t1.start();
// t1.join();
// t2.start();
// t2.join();
System.out.println(count);
}
}
运行的结果
我们可以发现每次运行的结果都是不一样的。
这样的代码。很明显就是有bug,是多线程引起的bug,这样的bug,就称为“线程安全问题”
如果我们把两个线程改为串行执行的话
代码运行结果与预期效果相符,所以很明显,当前的bug是由于多线程并发执行代码引起的bug,这样的bug,我们称为线程安全问题,或者是线程不安全。
我们可以站在CPU执行指令的角度上
- load,把内存中的值(count变量),读取到CPU寄存器上
- add,把指定寄存器中的值,进行+1操作(结果还是在这个寄存器中)
- save,把寄存器中的值,写回到内存中。
操作系统对线程的调度是随机的
我们可以画几种情况(但是不只是这几种情况,还有别的情况)
注:实际上每个线程调走,都可能有其他的更多的线程,甚至是背的进行的线程占用cpu执行
线程安全问题的产生原因
1.根本原因:操作系统对于线程的调度是随机的,抢占式执行
2.多个线程同时修改同一个变量
3.修改的操作不是原子的
4.内存可见性问题。引起的线程不安全
5.指令的重排序,引起的线程不安全
通过加锁操作,让不是原子的操作,打包成一个原子的操作。
对于上面的代码,我们可以进行加锁的操作,count++之前,先进行加锁,然后进行count++,计算完毕之后,再进行解锁。
加锁/解锁本身都是操作系统的api,很多操作系统都对这样的api进行了封装
synchronized关键词
Java中采用synchronized这样的关键字,搭配代码块,来实现类似的效果
注:在Java中,任何一个对象都可以用做"锁"
主要的是,是否有多个对象在竞争一个锁
public class Demo15 {
public static int count;
public static void main(String[] args) throws InterruptedException {
Object locker1=new Object();
Thread t1=new Thread(()->{
for (int i = 0; i < 5000; i++) {
synchronized (locker1){
count++;
}
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 5000; i++) {
synchronized (locker1){
count++;
}
}
});
t1.start();
t1.join();
t2.start();
t2.join();
System.out.println(count);
}
}
注:两个线程争对同一个对象加锁,才会产生互斥的效果(一个线程加上锁了,另一个线程就得阻塞等待,等到第一个线程释放锁,才有机会进行加锁)
可以使用synchronized的修饰方法
class Counter{
private int count;
public /*synchronized*/ void add(){
count++;
}
public int get(){
return count;
}
public synchronized void fun(){//加锁
//synchronized (Counter.class);
}//解锁(只要是出了大括号)
}
public class Demo18 {
public static void main(String[] args) throws InterruptedException {
Counter counter=new Counter();
Object locker=new Object();
Thread thread=new Thread(()->{
for (int i = 0; i < 50000; i++) {
synchronized (locker) {
counter.add();
}
}
});
Thread thread2=new Thread(()->{
for (int i = 0; i < 50000; i++) {
synchronized (locker) {
counter.add();
}
}
});
thread.start();
thread2.start();
thread.join();
thread2.join();
System.out.println("count="+counter.get());
}
}
synchronized修饰普通方法,相当于给this加锁
synchronized修饰静态方法,相当于给类对象加锁
可重复锁
class Counter2{
private int count;
public void add(){
count++;
}
public int get(){
return count;
}
}
public class Demo19 {
public static void main(String[] args) throws InterruptedException {
Counter2 counter2=new Counter2();
Thread t=new Thread(()->{
for (int i = 0; i < 50000; i++) {
synchronized (counter2){
synchronized (counter2){
synchronized (counter2){
counter2.add();
}
}
}
}
});
t.start();
t.join();
System.out.println("counter2="+counter2.get());
}
}
阻塞等待:
一旦方法调用的太深就会出现阻塞等待,想要解除阻塞,就需要往下执行,等到第一次的锁被释放,这样的问题,称为"死锁"。
为了解决上述的问题,Java的synchronized就引入可重入的概念(当线程针对一个锁,加锁成功之后,后续该线程再次针对这个锁进行加锁,不会触发阻塞,而是直接的往下走)
可重入锁的原理,关键是让锁对象内部保存,当前是哪个线程持有这把锁,后续的有线程争对这把锁加锁的时候,对比一下,是否是同一个锁。
关于死锁的出现可能
一个线程,一把锁,连续的加锁两次。
两个线程两把锁,每一个线程获得一把锁之后,都尝试获得对方的锁
//死锁
//eg:线程1和线程2都有自己的一把锁,但是都想去争对方的锁,导致死锁
public class Demo20 {
public static void main(String[] args) throws InterruptedException {
Object locker1=new Object();
Object locker2=new Object();
Thread t1=new Thread(()->{
try {
Thread.sleep(1000);
synchronized (locker1){
// synchronized (locker2){
// System.out.println("线程1争抢线程2的锁成功");
// }
}
} catch (InterruptedException e) {
e.printStackTrace();
}
//把嵌套的锁,改为并列的锁,就可以避免死锁
synchronized (locker2){
System.out.println("线程1争抢线程2的锁成功");
}
});
Thread t2=new Thread(()->{
try {
Thread.sleep(1000);
// synchronized (locker2){
// synchronized (locker1){
// System.out.println("线程2争抢线程1的锁成功");
// }
// }
//约定好加锁的顺序,就可以避免死锁
synchronized (locker1){
synchronized (locker2){
System.out.println("线程2争抢线程1的锁成功");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
}
//由于发生了死锁,所以无法成功输出
}
注:如果不加sleep(),很可能t1把locker1和locker2都拿到了,t2还没开始拿呢。
死锁的第三种情况,哲学家就餐问题
构成死锁的四个必要条件
1.锁是互斥的(锁的基本性质),一个线程拿到锁之后,另一个线程尝试拿到锁,必须要阻塞等待
2.锁是不可以抢占的,线程1拿到锁,线程2想要拿到锁,必须阻塞等待。
3.请求和保持(一个线程拿到锁1之后,不释放锁1,就去拿锁2)
4.循环等待,多个线程,多把锁之间的等待过程,构成了循环等待。(约定好加锁的顺序就可以破除循环等待了)
内存的可见性
造成线程安全的问题之一
public class Demo21 {
private static int flg=0;
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
while (flg==0){
}
System.out.println("线程结束");
});
Thread t2=new Thread(()->{
Scanner scanner=new Scanner(System.in);
System.out.println("请输入flg的值");
flg=scanner.nextInt();
});
t1.start();
t2.start();
}
}
运行结果
发现没有执行到 System.out.println("线程结束");
很明显,这也是一个bug,线程的安全问题。
产生的原因:编译器会对代码进行自动的优化,对你的代码进行调整,是程序效率更高,但是虽然是声称优化操作,保证逻辑不变,但是尤其在多线程的程序中,编译器的判断可能会出现错误,导致优化后的逻辑,与实际的逻辑发生偏差。
修改的方式引入sleep,直接占用1ms,此时优化flag无足轻重
也可以使用volatile关键词
通过这个关键词的来修饰某个变量,编译器对这个变量的读取操作,都不会优化成读寄存器。
public class Demo21 {
//采用关键字volatile
private volatile static int flg=0;
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
while (flg==0){
//sleep也可以解决优化的问题,但是很影响效率
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
System.out.println("线程结束");
});
Thread t2=new Thread(()->{
Scanner scanner=new Scanner(System.in);
System.out.println("请输入flg的值");
flg=scanner.nextInt();
});
t1.start();
t2.start();
}
}
成功运行
希望能对大家有所帮助!!!!