JAVA EE_多线程-初阶(二)

1.线程安全

1.1.观察线程不安全

实例:

java 复制代码
package thread;
public class text18 {
    //定义一个成员变量count,初始值为0
    private static int count=0;
    public static void main(String[] args) throws InterruptedException {
        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();
        System.out.println(count);
    }
}

以上代码中,我们理想的count结果是为10000,但是我们看下真实情况。

结果1:
结果2:

可以看到,两个运行结果不仅不是10000,而且两个运行结果还不相同,这便是线程不安全

1.2.线程安全的概念

一段代码,在多线程的运行下,如果它运行出来的结果与单线程运行出来的结果相同时,那么我们就可以说这个线程是安全的。

1.3.线程不安全的原因

线程随机性

  • 多线程的调度是随机性的,这个随机性就是线程不安全的主要因素。
  • 线程调度在一个环境下,它执行的顺序是不相同的。

修改公共数据

例如1.1中,两个线程修改一个的count变量。

这时候的count变量就是可以被多个线程修改的"公共数据"。

原子性

什么是原子性

我们可以把一段代码想象成一个的厕所,这时候张三去上厕所没把门锁住,正在进行到一半,老六进来了在张三背后说我也要上厕所,这时候张三就给老六吓得强行打断施法,并对老六破口大骂。

同时我们把1.1的例子带入到张三老六身上,我们就可以说1.1例子中的代码没有原子性;

但如果张三把门锁了,也就是t2的线程等t1的线程运行完之后再运行,我们就可以说这段代码有原子性

我们会把以上现象称为"同步互斥",表示这个操作是互相排斥的。

一条JAVA语句不一定是一条指令

在1.1的例子中,count就有三条指令

  • load 把内存中的值加载到寄存器中
  • add 在寄存器中进行+1的操作
  • save 将寄存器中操作过后的值放回内存中
不保证原子性会带来什么问题

例如1.1中,最后的cont就达不到我们理想中的数值

其原因就是t2的线程在t1还没完整的运行load,add,save这三个指令后就打断t1,使它得出错误的结果。

可见性

可见性是指,一个线程中的变变量能够被其他线程所看到。

1.4.解决之前的线程不安全问题

加个synchroized关键字

java 复制代码
package thread;
public class text19 {
    //创建count成员变量
    public static int count=0;
    //创建静态的Object类的lock(锁)
    public static final  Object lock=new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(()->{
            for(int i=0;i<5000;i++){
                //使用synchronized来锁定lock对象
                synchronized(lock){
                    count++;
                }                
            }
        });
        Thread t2=new Thread(()->{
            for(int i=0;i<5000;i++){
                //使用synchronized来锁定lock对象
                synchronized(lock){
                    count++;
                }                
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

2.synchroized 关键字

2.1.synchroized的特征

互斥

synchroized具有互斥性,当一个线程使用synchroized锁住一个对象,同时另一个线程也用synchroized锁住同一个对象,这时候另一个线程就会堵塞。

可以把一个线程想象成一个厕所,当李四进入厕所时,他就会使用sychroized将门锁住,这时候王五想上厕所,那么因为门给锁住了,王五只能在门外,等李四上完之后,王五就获得了李四的锁,然后就进去上厕所再把门锁上,就不会像上文中张三老六发生尴尬的现象。

同理,例如1.1例子中,如果两个线程都用sychroized锁住了同一个对象,那么只能等t1线程解锁运行完之后,然后t2获得了锁再运行。

注意:

在上一个线程运行完解锁之后,如果有多个线程,那么系统会随机唤醒某个线程,而不是按照写代码的顺序唤醒,例如如果有t1,t2,t3线程,在t1线程解锁之后,等待的t2和t3线程中的其中一个就会获得锁,可能是t2也可能是t3。

可以理解为李四上完厕所出来手上拿着锁,在门外的张三和老六都想上厕所,然后他们就开始了争夺锁,最后可能是老六得到了锁也可能是张三得到了锁,但不管是谁得到了锁,到了最后他们都能成功的上完厕所~~

可重入

synchronized对于同一个线程来说是可重入的,就是可以写多条synchronized语句,不会造成死锁的情况。

死锁:

死锁就是写入一条syncronized语句时,还没解锁就写入另一条synchronized,这时候如果要解锁里面的syncronized就需要先解锁外面一层的synchronized,然而理想是丰满的现实是残酷的,有两个条锁的情况下我们并不能把这两条锁都解除掉,当第一个锁解除后这条线程也就停止了运行,第二个锁就解不了了,这就造成了死锁现象

但是JAVA的synchronized是可重入锁,这就为我们解决了死锁的问题。

在可重入中,包含了"线程持有者"和"计时器"两个信息

"线程持有者"记录了当前的前程,当发现有线程占用锁时,但那个占用锁的人是自己本身,那么仍可以获取到锁,并让"计时器"加1;

解锁的时候,当计时器为0时,这条线程才为解锁状态。

2.2.synchronized使用示例

基础用法:

java 复制代码
        Thread t1=new Thread(()->{
            for(int i=0;i<5000;i++){
                //使用synchronized锁住lock对象
                synchronized(lock){
                    count++;
                }
            }
        });

变为方法使用:

java 复制代码
//1.创建一个方法,在方法内调用synchronized
     public static void add(){
        synchronized(lock){
            count++;
        }
    }

//2.创建一个synchronized 类的方法
    synchronized public static void add(){
        count++;
    }

2.3.JAVA标准库中的线程安全类

JAVA库中的很多都是线程不安全的,他们都涉及数据的共享与修改,以下是一些线程安全的

  • Vector
  • HashTable
  • ConcurrenHashMap
  • StringBuffer

3.volatile关键字

3.1.volatile能保证内存可见性

实例:

java 复制代码
package thread;
import java.util.Scanner;
//写两个线程,通过t2线程来改变t1线程
public class Demo27 {
    //定义f成员变量
    static int f=0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(()->{
            //f=0时,while循环一直运行0
            while(f==0){
                //dosomthing
            }
            System.out.println("t1线程结束");
        });
        
        Thread t2=new Thread(()->{
            Scanner in = new Scanner(System.in);
            //改变f的值,让t1的while循环结束
            System.out.println("请输入一个数");
            f=in.nextInt();
            System.out.println("t2线程结束");
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

我们理想的结果应该是输入任意一个非0值,然后打印"t2线程结束""t1线程结束",可结果却是

只打印了 "t2线程结束",t1的线程还在运行,那么我们就可以通过该结果得出我们写的代码有一定的问题的

这就涉及到了"可见性",在t2线程中修改的f变量,t1中没有发现

这其实也是编译器的优化,所谓的编译器优化是指如果大量读取一个相同的数据,那么接下来都会默认是该数据

首先每个JAVA进程都有一个"主内存",而每个JAVA线程都有自己的"工作内存",每个JAVA线程先从内存中读取数据到自己的工作内存,然后进行工作

所以上述代码中,t1线程将f=0读取到自己的工作内存(此时触发了编译器优化),然而t2修改的是主内存中的数据,t1的工作内存中的数据仍然是f=0,两者不会有影响。

这时候我们就需要使用volatile

java 复制代码
public class Demo27 {
    //在f成员变脸添加volatile
    static volatile int  f=0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(()->{
            while(f==0){

            }
            System.out.println("t1线程结束");
        });
        
        Thread t2=new Thread(()->{
            Scanner in = new Scanner(System.in);
            System.out.println("请输入一个数");
            f=in.nextInt();
            System.out.println("t2线程结束");
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}
结果:
  • 代码运行时volatile从主内存读取最新的值返回给java线程的工作内存中
  • volatile可以保证"内存可见性"
  • 我们可以理解为使用volatile相当于告诉编译器让他停止优化功能
  • 虽然运行速度慢了,但是数据更准确了

3.2.volatile不能保证原子性

  • volatile和synchroized有本质区别
  • volatile:保证"内存可见性"
  • synchroized:保证"原子性"

4.wait和notify

由于线程之间是抢占式执行,所以线程的执行顺序是不稳定的,但我们需要使线程之间有序进行,所以就需要wait()和notify()两个方法。

4.1.wait()方法

功能:

  • 让当前进程等待运行
  • 释放当前的锁
  • 当满足一定条件时被唤醒,并尝试重新获得锁

结束条件:

  • 使用notify()唤醒
  • wait自带的等待时间超时(wait中自带timeoutMillis,设置等待的时间)

实例:

java 复制代码
package thread;
public class test20 {
    public static void main(String[] args) throws InterruptedException {
        //创建锁
        Object lock=new Object();
        //创建线程
        Thread t1=new Thread(()->{
            System.out.println("wait之前");
            //让线程等待
            //因为wait方法必须在同步代码块中使用
            //所以要先获取锁
            synchronized(lock){      
                try{
                    lock.wait();
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
            System.out.println("wait之后");
        });
        //启动线程
        t1.start();
    }
}
结果:

以上代码中运行后只打印了"wait之前",我们再来看一下JAVA的管理控制台

可以看到我们创建的该线程的状态为等待状态

那么就说明了wait方法是将当前线程变为等待状态

4.2.notify()方法

功能:

  • 唤醒一个正在等待的线程
  • 只能唤醒一个等待的线程,如果由多个等待线程,notify则随机唤醒一个
  • 在notify之后,不会马上释放锁,而是等待notify当前的同步代码块运行完才释放锁

实例1:

创建一个等待线程和一个唤醒线程

java 复制代码
package thread;
public class test20 {
    public static void main(String[] args) throws InterruptedException {
        //创建锁
        Object lock=new Object();
        //创建线程--等待线程
        Thread t1=new Thread(()->{
            System.out.println("wait之前");
            //让线程等待
            //因为wait方法必须在同步代码块中使用
            //所以要先获取锁
            synchronized(lock){      
                try{
                    lock.wait();
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
            System.out.println("wait之后");
        });

        //创建线程--唤醒线程
        Thread t2=new Thread(()->{
            System.out.println("notify之前");
            //唤醒线程
            //因为notify方法必须在同步代码块中使用
            //所以要先获取锁
            synchronized(lock){
                lock.notify();
            }
            System.out.println("notify之后");
        });

        //启动线程
        t1.start();
        t2.start();
    }
}
结果:

实例2:

创建两个等待线程和一个唤醒线程

java 复制代码
package thread;
public class text22 {
    public static void main(String[] args) throws InterruptedException {
        //创建锁
        Object lock=new Object();
        //创建线程--等待线程
        Thread t1=new Thread(()->{
            System.out.println("wait1之前");

            synchronized(lock){      
                try{
                    lock.wait();
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
            System.out.println("wait1之后");
        });

        Thread t2=new Thread(()->{
            System.out.println("wait2之前");

            synchronized(lock){      
                try{
                    lock.wait();
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
            System.out.println("wait2之后");
        });

        //创建线程--唤醒线程
        Thread t=new Thread(()->{
            System.out.println("notify之前");
            //唤醒线程
            //因为notify方法必须在同步代码块中使用
            //所以要先获取锁
            synchronized(lock){
                lock.notify();
            }
            System.out.println("notify之后");
        });

        //启动线程
        t1.start();
        t2.start();
        t.start();
    }

}
结果:

运行后只唤醒了t1线程

所以notify只能唤醒一个线程

4.3.notifyAll ()方法

功能:

  • 唤醒所有线程

实例:

创建三个等待线程和一个唤醒线程

java 复制代码
package thread;

public class text21 {
    public static void main(String[] args) throws InterruptedException {
        Object lock=new Object();
        Thread t1=new Thread(()->{
            System.out.println("wait1之前");
            synchronized (lock){
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("wait1之后");
        });

        Thread t2=new Thread(()->{
            System.out.println("wait2之前");
            synchronized(lock){
                try{
                    lock.wait();
                }catch (InterruptedException e){
                    throw new RuntimeException(e);

                }
            }
            System.out.println("wait2之前");
        });

        Thread t3=new Thread(()->{
            System.out.println("wait3之前");
            synchronized(lock){
                try{
                    lock.wait();
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
            System.out.println("wait3之前");
        });

        Thread t=new Thread(()->{
            System.out.println("notifyAll之前");
            synchronized(lock){
                lock.notifyAll();
            }
            System.out.println("notifyAll之前");
        });

        t1.start();
        t2.start();
        t3.start();

        t.start();
    }
}
结果:

可以看到所有等待线程都被唤醒,但是唤醒的顺序不同,因为是同时唤醒,notify一时间不知道先该唤醒谁,所以就导致了线程竞争。

4.4.notify 和 notifyAll的对比

可以想象notify/notifyAll和wait为老师和学生

notify老师检查背诵的时候,喜欢一个一个按号数叫wait学生起来背诵

notifyAll老师检查背诵的时候则是:"你们谁要来背诵,先背完的就可以走了",那么好几个wait学生就争抢着取背诵~~

  • notify为唤醒单个线程,有序
  • notifyAll为同时唤醒多个线程,无序

4.5.wait 和 sleep 的对比

相同点:

  • wait和sleep都可以使线程等待一段时间
  • 都有超时时间。

不同点:

  • wait是Object下的方法,sleep是Thread下的静态方法
  • wait它的设计是为了提前唤醒,sleep它的设计是为了到点唤醒
  • wait提前唤醒时不会报错,sleep提前唤醒时会报错
  • wait需要上锁,sleep不需要上锁
相关推荐
帮帮志9 分钟前
Python代码list列表的使用和常用方法及增删改查
开发语言·python
柳鲲鹏13 分钟前
Could not find artifact com.microsoft.sqlserver:sqljdbc4:jar:4.0 in central
java·jar
前进的程序员17 分钟前
AI 时代:哪些开发语言将引领潮流
开发语言·人工智能
烁3471 小时前
每日一题(小白)模拟娱乐篇13
java·算法·娱乐·暴力
2301_793069821 小时前
前后端分离下,Spring Boot 请求从发起到响应的完整执行流程
java·spring boot·mvc
Knock man1 小时前
QML和C++交互
开发语言·c++·交互
褚瑱琅1 小时前
T-SQL语言的压力测试
开发语言·后端·golang
烁3471 小时前
每日一题(小白)模拟娱乐篇14
java·开发语言·算法·娱乐·暴力
✿ ༺ ོIT技术༻1 小时前
C++11:lambda表达式
开发语言·c++
xiaolingting3 小时前
Java 二叉树非递归遍历核心实现
java··二叉树非递归遍历