【Java JUC】Java多线程并发编程零基础小白入门(中)
前言
在被面试官疯狂打击后,我决定重新系统学习JUC编程,也就是并发编程。本篇文章作为Java语言多线程并发编程的入门级别,仅从学会使用的角度学习,并不深入涉及有关知识的底层实现原理,比如线程池机制、synchronized关键字、ReentrantLock等。文章会穿插一些面试会问到的面试题。
上篇文章看这里:
问题演示
如果有多个线程在同时运行,而这些线程可能会同时去访问一些临界资源。如果程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
线程安全问题都是由多个线程去访问临界资源比如全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
线程不安全示例:
Account.java
public class Account {
private String accountID;
private Integer money;
public Account(String accountID, Integer money){
this.accountID = accountID;
this.money = money;
}
public void WithdrawMoney(String name, Integer money){
if (money > this.money){
System.out.println(name + "来取钱,余额不足!");
return;
}
System.out.println(name + "来取钱,当前余额为" + this.money);
this.money -= money;
System.out.println(name + "已成功取走" + money + "元, 当前余额为" + this.money);
}
}
WithdrawThread.java
public class WithdrawThread implements Runnable{
private Account account;
public WithdrawThread(Account account){
this.account = account;
}
@Override
public void run() {
account.WithdrawMoney(Thread.currentThread().getName(), 10000);
}
}
Main.java
public class Main {
public static void main(String[] args) {
Account account = new Account("BWFW0001", 10000);
new Thread(new WithdrawThread(account), "小米").start();
new Thread(new WithdrawThread(account), "小美").start();
}
}
// 执行结果
小米来取钱,当前余额为10000
小美来取钱,当前余额为10000
小米已成功取走10000元, 当前余额为0
小美已成功取走10000元, 当前余额为-10000 // 线程不安全导致的错误余额
线程同步
是为了解决线程安全问题。
多个线程
访问同一资源
的时候,且多个线程中对临界资源有写的操作
,就容易出现线程安全问题。要解决上述多线程并发访问一个资源的安全性问题,Java中提供了同步机制
(synchronized)来解决。
为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。
那么怎么去使用呢?有三种方式完成同步操作:
-
同步代码块。
-
同步方法。
-
Lock锁机制。
同步代码块
同步代码块:synchronized
关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
格式:
synchronized(同步锁){
需要同步操作的代码
}
同步锁:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.
-
锁对象 可以是任意类型。
-
多个线程对象 要使用同一把锁。
使用同步代码块解决代码:
public void WithdrawMoney(String name, Integer money){
synchronized (this){
if (money > this.money){
System.out.println(name + "来取钱,余额不足!");
return;
}
System.out.println(name + "来取钱,当前余额为" + this.money);
this.money -= money;
System.out.println(name + "已成功取走" + money + "元, 当前余额为" + this.money);
}
}
同步方法
同步方法:使用synchronized
修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
格式:
public synchronized void method(){
// 可能会产生线程安全问题的代码
}
同步锁是谁?
对于非static方法,同步锁就是this。
对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
使用同步方法代码如下:
public synchronized void WithdrawMoney(String name, Integer money){
if (money > this.money){
System.out.println(name + "来取钱,余额不足!");
return;
}
System.out.println(name + "来取钱,当前余额为" + this.money);
this.money -= money;
System.out.println(name + "已成功取走" + money + "元, 当前余额为" + this.money);
}
Lock锁机制
java.util.concurrent.locks.Lock
机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大
Lock锁也称同步锁,加锁与释放锁方法化了,如下:
-
public void lock()
:加同步锁。 -
public void unlock()
:释放同步锁。
使用如下:
private ReentrantLock lock = new ReentrantLock();
public void WithdrawMoney(String name, Integer money){
lock.lock();
if (money > this.money){
System.out.println(name + "来取钱,余额不足!");
return;
}
System.out.println(name + "来取钱,当前余额为" + this.money);
this.money -= money;
System.out.println(name + "已成功取走" + money + "元, 当前余额为" + this.money);
lock.unlock();
}
线程状态
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有几种状态呢?在API中java.lang.Thread.State
这个枚举中给出了六种线程状态:
这里先列出各个线程状态发生的条件,下面将会对每种状态进行详细解析。
线程状态 | 导致状态发生条件 |
---|---|
NEW(新建) | 线程刚被创建,但是并未启动。当我们使用new关键字去实例化一个Thread时,就创建了线程对象,此时该线程对象就处于NEW(新建状态)。此时还没调用start方法,只有线程对象,没有线程特征。 |
Runnable(可运行) | 当处于NEW状态的线程通过调用start方法后,该线程就进入了Runnable(可运行)状态,这种状态对应于操作系统中线程的就绪态和运行态两种状态。也就是说处于Runnable状态下的线程,并不意味着正在执行,可能因未被CPU调度而处于就绪态。 |
Blocked(锁阻塞) | 当处于Runnable状态的线程在执行时需要访问被synchronized修饰的临界资源时,试图获取一个对象锁时,如果该对象锁被其他的线程持有,未能获得当前对象锁的monitor监视器,那么该线程就进入Blocked(锁阻塞)状态。当该线程持有锁时,该线程将变成Runnable状态。 |
Waiting(无限等待) | 当处于Runnable状态下的线程执行wait()、join()或者执行LockSupport.park()方法时,就会进入Waiting(无限等待)状态,进入这个状态后是不能自动唤醒的,只有当执行了LockSupport.unpark(),或者join的线程运行结束,或者被中断时才可以进入Runnable状态。如果另一个线程调用notify或者notifyAll方法才能够唤醒进入Blocked (锁阻塞)状态而不是Runnable状态。 |
Timed Waiting(计时等待) | 同waiting状态,区别仅在于有没有时间限制,Timed waiting 会等待超时,由系统自动唤醒,或者在超时前被唤醒信号唤醒。 |
Teminated(被终止) | 当run()方法执行完毕,线程正常退出。或者出现一个没有捕获的异常,终止了run()方法,最终导致意外终止。 |
参考答案见上方表格和图片。
sleep方法
我们看到状态中有一个状态叫做计时等待,可以通过Thread类的方法来进行演示.
public static void sleep(long time)
让当前线程进入到睡眠状态,到毫秒后自动醒来继续执行
public class SleepTest {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + "开始进入Sleep....");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "结束Sleep....");
}
}
这时我们发现主线程执行到sleep方法会休眠3秒后再继续执行。
TimeUnit
TimeUnit是java.util.concurrent
包下面的一个类,TimeUnit
提供了可读性更好的线程暂停操作,通常用来替换Thread.sleep( ) 底层实现还是使用的Thread.sleep()
字段 | 描述 |
---|---|
SECONDS | 停顿3秒 |
MINUTES | 停顿3分钟 |
HOURS | 停顿3小时 |
DAYS | 停顿三天 |
代码使用:
//停顿3s
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) {e.printStackTrace();}
//停顿3分钟
try { TimeUnit.MINUTES.sleep(3); } catch (InterruptedException e) {e.printStackTrace();}
//停顿3h
try { TimeUnit.HOURS.sleep(3); } catch (InterruptedException e) {e.printStackTrace();}
//停顿三天
try { TimeUnit.DAYS.sleep(3); } catch (InterruptedException e) {e.printStackTrace();}
线程通信
wait/notify/notifyAll方法
Object类的方法
public void wait()
: 让当前线程进入到等待状态 此方法必须锁对象调用。
public void notify()
: 唤醒当前锁对象上等待状态的线程 此方法必须锁对象调用。
public class Wait_Notify {
public static void main(String[] args) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "进入Wait.....");
synchronized (""){
try {
"".wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "已被Notify.....");
}, "线程一").start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "唤醒Wait.....");
synchronized (""){
"".notify();
}
}, "线程二").start();
}
}
生产者/消费者案例
定义一个集合,包子铺线程完成生产包子,包子添加到集合中;吃货线程完成购买包子,包子从集合中移除。
-
当包子没有时,吃货线程等待.
-
包子铺线程生产包子,并通知吃货线程(解除吃货的等待状态)
Baozi.java
public class BaoZi {
private String name;
private Double money;
public BaoZi() {
}
public BaoZi(String name, Double money) {
this.name = name;
this.money = money;
}
/**
* 获取
* @return name
*/
public String getName() {
return name;
}
/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}
/**
* 获取
* @return money
*/
public Double getMoney() {
return money;
}
/**
* 设置
* @param money
*/
public void setMoney(Double money) {
this.money = money;
}
public String toString() {
return "BaoZi{name = " + name + ", money = " + money + "}";
}
}
public class BaoZiPu extends Thread{
private ArrayList<BaoZi> list;
private String name;
public BaoZiPu(String name, ArrayList<BaoZi> list) {
super(name);
this.list = list;
}
@Override
public void run() {
String ThreadName = Thread.currentThread().getName();
while (true){
synchronized (list){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() < 11){
// 没有包子了,需要做
BaoZi baozi = getBaozi();
list.add(baozi);
System.out.println(ThreadName + "做了一个" + baozi.getName());
} else{
System.out.println("包子做满了,需要消费....");
list.notifyAll();
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
private BaoZi getBaozi(){
int randomNum = (int) (Math.random() * 10);
if (randomNum > 8) return new BaoZi("韭菜包子", 1.0);
if (randomNum > 5) return new BaoZi("猪肉包子", 2.0);
if (randomNum >= 0) return new BaoZi("豆沙包子", 1.0);
return new BaoZi("异常包子", 100.0);
}
}
public class Customer extends Thread{
private ArrayList<BaoZi> list;
private String name;
public Customer(String name, ArrayList<BaoZi> list) {
super(name);
this.list = list;
}
@Override
public void run() {
String ThreadName = Thread.currentThread().getName();
while (true){
synchronized (list){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() > 0){
// 有包了, 可以吃
BaoZi baoZi = list.remove(0);
System.out.println(ThreadName + "吃了一个" + baoZi.getName());
} else{
System.out.println("没有包子了,需要生产....");
list.notifyAll();
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
public class Main {
public static void main(String[] args) {
ArrayList<BaoZi> list = new ArrayList<>();
new BaoZiPu( "包子铺", list).start();
new Customer("小米", list).start();
}
}
4、volatile关键字
内存不可见性案例
public class Demo {
private static Boolean flag = false;
public Demo() {
}
public static Boolean getFlag() {
return flag;
}
public static void setFlag(Boolean flag) {
Demo.flag = flag;
}
public static void main(String[] args) {
new Thread(new MyRunnable()).start();
while (true){
if (flag){
System.out.println("主线程flag =" + flag);
}
}
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("子线程flag =" + Demo.getFlag());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
Demo.setFlag(true);
System.out.println("子线程flag =" + Demo.getFlag());
}
}
子线程flag =false 子线程flag =true
【程序陷入死循环.....】
主线程flag = true
,这是为什么呢?明明在子线程中进行了修改为true了啊-
子线程从主内存读取到数据放入其对应的工作内存
-
将flag的值更改为true,但是这个时候flag的值还没有写回主内存
-
此时main方法读取到了flag的值为false
-
问题解决
加锁
为了搞清楚原因,我们需要了解下什么时JMM了。
JMM
概述:JMM(Java Memory Model)Java内存模型
,是java虚拟机规范中所定义的一种内存模型。
-
所有的共享变量都存储于主内存
。这里所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。 -
每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。
-
线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问
-
对方工作内存中的变量,线程间变量的值的传递需要通过主内存完成。
然而,JMM这样的规定可能会导致线程对共享变量的修改没有即时更新到主内存,或者线程没能够即时将共享变量的最新值同步到工作内存中,从而使得线程在使用共享变量的值时,该值并不是最新的。正因为JMM这样的机制,就出现了不可见性问题。那我们要如何解决不可见性问题呢?
问题分析
-
子线程从主内存读取到数据放入其对应的工作内存
-
将flag的值更改为true,但是这个时候flag的值还没有写回主内存
-
此时main方法读取到了flag的值为false
-
问题解决
加锁
public static void main(String[] args) {
new Thread(new MyRunnable()).start();
while (true){
synchronized (""){
if (flag){
System.out.println("主线程flag =" + flag);
}
}
}
}
某一个线程进入synchronized代码块前后,执行过程入如下:
b.清空工作内存
c.从主内存拷贝共享变量最新的值到工作内存成为副本
d.执行代码
e.将修改后的副本的值刷新回主内存中
f.线程释放锁
volatile关键字
private volatile static Boolean flag = false;
-
子线程从主内存读取到数据放入其对应的工作内存
-
将flag的值更改为true,但是这个时候flag的值还没有写会主内存
-
此时main方法main方法读取到了flag的值为false
-
当子线程将flag的值写回去后,失效其他线程对此变量副本
-
再次对flag进行操作的时候线程会从主内存读取最新的值,放入到工作内存中
使用volatle修饰共享变量后,每个线程要操作变量时会从主内存中将变量拷贝到本地内存作为副本,当线程操作变量副本并写回主内存后,会通过CPU总线嗅探机制告知其他线程该变量副本已经失效,需要重新从主内存中读取。
总线嗅探机制
在现代计算机中,CPU的速度是极高的,如果CPU需要存取数据时都直接与内存打交道,在存取过程中,CPU将一直空闲,这是一种极大的浪费,所以,为了提高处理速度,CPU不直接和内存进行通信,而是在CPU与内存之间加入很多寄存器,多级缓存,它们比内存的存取速度高得多,这样就解决了CPU运算速度和内存读取速度不一致问题,
由于CPU与内存之间加入了缓存,在进行数据操作时,先将数据从内存拷贝到缓存中,CPU直接操作的是缓存中的数据。但在多处理器下,将可能导致各自的缓存数据不一致(这也是可见性问题的由来),为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,而嗅探是实现缓存一致性的常见机制。
注意:基于CPU缓存一致性协议,JVM实现了volatile的可见性,但由于总线嗅探机制,会不断的监听总线,如果大量使用volatile会引起总线风暴。所以,volatile的使用要适合具体场景。
volatile在单例模式中的应用
单例模式有8种,而懒汉式单例双重检测模式中就使用到了volatile关键字。 代码如下:
class Singleton1 {
// 第一步:私有化构造器
private Singleton1(){}
// 第二步:创建实例对象变量,此时并不去初始化
private static volatile Singleton1 instance;
// 第三步:提供一个静态的公有方法获取实例对象
public static Singleton1 getInstance(){
// 用到时,第一次判断当前实例对象是否存在,若不存在则进行初始化
if (instance == null){
// 加入同步处理的代码块,解决线程安全问题
synchronized (Singleton1.class){
if (instance == null){
instance = new Singleton1();
}
}
}
// 第一次判断不为空则直接返回对象实例
return instance;
}
}
volatile关键字总结
-
volatile关键字除了防止JVM的指令重排。还有一个重要的作用就是保证变量的可见性。任何一个线程对其的修改将立马对其他线程可见。volatile修怖符适用于以下场景∶某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值;或者作为状态变量,如flag = ture,实现轻量级同步。
-
volatile只能作用于属性变量,我们用volatile修饰属性,这样编译器就不会对这个屋性做指令重排序而且还可以保证可见性。
-
volatile属性的渎写操作都是无锁的,它不能替代 synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的。
面试题:说说synchronized关键字和volatile关键字的区别
首先说JMM当中并发编程的三个重要特性:
-
原子性:在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。
-
可见性:当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。
-
有序性:代码在执行的过程中的先后顺序,Java在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。
synchronized关键字和volatile 关键字是两个互补的存在,而不是对立的存在!,区别如下:
-
可见性方面: synchronized和volatile关键字都可以保证共享变量的可见性。
-
原子性方面: synchronized可以保证代码片段的原子性。
-
volatile可以使纯赋值操作是原子的,如boolean flag = true; falg = false。
-
但volatile 不可以保证代码片段的原子性,比如类似于flag = !flag这种复合操作
-
-
有序性方面: volatile可以通过禁止指令重排序保证代码片段的执行有序性
另外,最后:
-
volatile关键字只能用于变量,而synchronized关键字可以修饰方法以及代码块
-
文章太长,影响阅读效果,后续内容请看下一篇。