一. 线程安全问题
线程安全问题:多线程同时对同一个全局变量做写的操作,可能会受到其他线程的干扰,就会发生线程安全性问题,涉及到java的内存结构,全局共享变量存储在堆中,堆内存是共享的,下面模拟两个线程冲突概率较大的情况,因为cpu是多核的所以当线程从阻塞状态到运行状态的时候两个线程会同时对全局变量进行操作那么发生冲突的概率就会大很多,可以发现输出结果的差异是比较大的:(多线程中一个核心的概念是同时执行)
package thread;
public class ThreadCount implements Runnable{
private int count = 100;
@Override
public void run() {
while (true){
if (count > 1){
try {
// 运行状态----休眠状态----cup的执行权让给其他的线程
Thread.sleep(3000);
// 休眠状态
}catch (Exception e){
}
count--;
System.out.println(Thread.currentThread().getName() + " " + count);
}
}
}
public static void main(String[] args) {
ThreadCount threadCount = new ThreadCount();
new Thread(threadCount).start();
new Thread(threadCount).start();
}
}
多线程如何解决线程安全问题/ 多线程如何实现同步呢?这两个问题属于同一个问题,核心思想:锁、分布式锁
在同一个 jvm 中,多个线程需要竞争锁的资源,最终只能够有一个线程能够获取到锁,多个线程同时抢同一把锁,谁(线程)能够获取到锁,谁就可以执行到该代码,如果没有获取锁成功 中间需要经历锁的升级过程,如果一致没有获取到锁则会一直阻塞等待。如果线程 A 获取锁成功 但是线程 A 一直不释放锁,线程 B 一直获取不到锁,则会一直阻塞等待。代码从那一块需要上锁? -----可能会发生线程安全性问题的代码需要上锁。juc 并发编程:锁,重入锁,悲观锁,乐观锁,公平锁,非公平锁。对一块代码加锁缺点:可能会影响到程序的执行效率,获取锁与释放锁全部都是有底层虚拟机实现好了。如果是同一把锁在多线程的情况下最终只能够给一个线程使用。如果有线程持有了该锁意味着其他的线程不能够在继续获取锁。如果是同一把锁在多线程的情况下最终只能够给一个线程使用。
加锁之后那么结果就是正确的:
package thread;
public class ThreadCount implements Runnable{
private int count = 100;
@Override
public void run() {
while (true){
if (count > 1){
// 加锁
synchronized (this){
count--;
System.out.println(Thread.currentThread().getName() + " " + count);
}
}
}
}
public static void main(String[] args) {
ThreadCount threadCount = new ThreadCount();
new Thread(threadCount).start();
new Thread(threadCount).start();
}
}
二. synchronized 锁的基本用法
线程如何实现同步?本质上问的是线程如何保证线程安全性的问题,主要包括以下几个方面:
1. 使用 synchronized 锁,从jdk1.6开始,锁的升级过程,偏向锁--->轻量级锁--->重量级锁;
2. 使用Lock锁(JUC),需要自己实现锁的升级过程,底层是基于aps和cas实现;
3. 使用ThreadLocal,需要注意内存泄漏的问题
4. 原子类CAS非阻塞式
Spring MVC Controller 默认是单例的(spring默认bean对象是单例的),所以需要注意线程安全问题,单例的原因有二:
1. 为了性能;
2. 不需要多例;
例如在控制器中有一个count()方法,当多个请求访问的时候,多个请求线程访问当前方法的过程中那么就会发生线程安全问题,这个时候就需要加锁保证线程安全问题,所有请求过来都会访问到count()方法,当多个请求进行访问的时候那么多个线程就会造成锁资源竞争的问题(在浏览器中多次刷新页面发送新的请求),由于spring mvc的Controller是单例的而且加上了this锁所以每个请求线程获取到锁之后那么其余的请求只能够等待,所以相当于是单线程:
package com.example.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class LockController {
private int count = 0;
@RequestMapping("/count")
public synchronized String count() throws InterruptedException {
// 每个请求花费3s那么3个请求总共花费9s的时间(刷新三次), 可以通过打印count的输出判断是否是单例的
Thread.sleep(3000);
System.out.println(count++);
return "count";
}
}
我们可以使用@Scope(value = "prototype")注解将当前的Bean对象修改为多例,这样每个请求过来就不会共享全局变量count,每个synchronized锁都是不同的this锁,所以不需要竞争锁资源:
import org.springframework.context.annotation.Scope;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Scope(value = "prototype")
public class LockController {
private int count = 0;
@RequestMapping("/count")
public synchronized String count() throws InterruptedException {
// 每个请求花费3s那么3个请求总共花费9s的时间(刷新三次)
Thread.sleep(3000);
System.out.println(count++);
return "count";
}
}
三. 多线程之间的通信--wait()和notify()方法
多线程之间的通信
wait()方法:释放锁资源, 同时当前线程会阻塞,例如下面的代码运行会报错,因为调用this.wait()的时候没有告诉锁对象,wait()和notify()方法需要结合synchronized对象锁使用,wait释放的是同步代码块中的对象锁,当前获取的是哪个对象锁那么就释放哪个对象锁,如果调用其他的锁会抛出异常:
package com.example.demo.controller;
public class Test01 {
public static void main(String[] args) throws InterruptedException {
new Test01().print();
}
public void print() throws InterruptedException {
// wait()方法: 释放锁资源, 同时当前线程会阻塞
this.wait();
}
}
加上synchronized锁,当主线程进入到print()方法的时候获取到this锁执行到this.wait()的时候释放锁资源同时主线程也会阻塞:
public class Test01 {
public static void main(String[] args) throws InterruptedException {
new Test01().print();
}
public void print() throws InterruptedException {
synchronized (this){
System.out.println("<1>");
System.out.println(Thread.currentThread().getName());
// wait()方法: 释放锁资源, 同时当前线程会阻塞
this.wait();
System.out.println("<2>");
}
}
}
并且需要注意的是wait()方法在调用的时候需要获取到锁的对象,也即锁的对象.wait(),因为当前是this锁所以需要使用this.wait()调用,当前是哪种锁那么使用谁来调用,例如下面的例子是string类型的lock锁那么使用lock.wait()调用:
public class Test01 {
private String lock = "lock";
public static void main(String[] args) throws InterruptedException {
new Test01().print();
}
public void print() throws InterruptedException {
// 当前是lock锁所以需要使用lock.wait()来调用
synchronized (lock){
System.out.println("<1>");
System.out.println(Thread.currentThread().getName());
lock.wait();
System.out.println("<2>");
}
}
}
下面的代码在运行3s之后主线程唤醒子线程之后会报错,这是因为notify()方法需要放到同步代码块里面:
public class Test01 {
private String lock = "lock";
public static void main(String[] args) throws InterruptedException {
new Test01().print();
}
public void print() throws InterruptedException {
// 使用匿名函数创建和启动一个子线程
new Thread(() -> {
synchronized (lock){
System.out.println("<1>");
System.out.println(Thread.currentThread().getName() + " ====>");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("<2>");
}
}).start();
// 主线程3s之后唤醒子线程
try {
Thread.sleep(3000);
lock.notify();
}catch (Exception e){
// 输出报错的信息
e.printStackTrace();
}
}
}
将notify()方法放到同步代码块里面那么就不会报错了(wait()和notify()方法需要放到同步代码块中):
public class Test01 {
private String lock = "lock";
public static void main(String[] args) throws InterruptedException {
new Test01().print();
}
public void print() throws InterruptedException {
// 使用匿名函数创建和启动一个子线程
new Thread(() -> {
synchronized (lock){
System.out.println("<1>");
System.out.println(Thread.currentThread().getName() + " ====>");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("<2>");
}
}).start();
// 主线程3s之后唤醒子线程
try {
Thread.sleep(3000);
synchronized (lock){
lock.notify();
}
}catch (Exception e){
// 输出报错的信息
e.printStackTrace();
}
}
}
注意主线程必须要休眠一段时间,因为main()方法调用print()函数的时候有可能在启动的子线程的时候,print()方法中的主线程已经执行完了,也即先执行了lock.notify()方法,此时再执行子线程中的方法那么就唤醒不了子线程了,所以需要调用Thread.sleep()方法暂停当前的主线程,把cpu片段让出给其他线程,减缓当前线程的执行,多次运行可能结果是不一样的:
public class Test01 {
private String lock = "lock";
public static void main(String[] args) throws InterruptedException {
new Test01().print();
}
public void print() throws InterruptedException {
// 使用匿名函数创建和启动一个子线程
new Thread(() -> {
synchronized (lock){
System.out.println("<1>");
System.out.println(Thread.currentThread().getName() + " ====>");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("<2>");
}
}).start();
// 下面的try-catch代码块属于主线程
try {
System.out.println(Thread.currentThread().getName() + " ====>");
synchronized (lock){
System.out.println("唤醒子进程");
lock.notify();
System.out.println("唤醒子进程结束");
}
}catch (Exception e){
// 输出报错的信息
e.printStackTrace();
}
}
}
输出结果可能为下面这两种情况,其实主线程和子线程是并行执行的,当先执行lock.notify()方法的时候子线程是无法被主线程唤醒的,当先执行子线程的时候那么执行到主线程中的lock.notify()方法之后那么就会被唤醒,唤醒之后输出"<2>"最终主线程结束,如果没有唤醒子线程那么子线程会一直阻塞无法退出:
1. 第一种输出结果:
main ====>
唤醒子进程
唤醒子进程结束
<1>
Thread-0 ====>
2. 第二种输出结果:
main ====>
<1>
Thread-0 ====>
唤醒子进程
唤醒子进程结束
<2>
...主进程以代码0退出
wait(),notify()生产者与消费者模型:
下面的例子中当由于Res是全局变量,所以当输入线程和输出线程共享全局变量的时候就有可能发生线程安全问题,下面的代码输出结果就是错误的,解决线程安全问题的一个方法是对发生线程安全的代码块加上锁,其实也好理解,因为Res是全局共享变量所以当输入线程执行的时候将username = "hexin",当切换到输出线程的时候那么输出当前全局res.username,然后可能又切换到输入线程那么将username = "xiaohong",再执行sex = '女',此时切换到输出线程那么输出sex = '女',所以就出现了错误:
public class Thread02 {
// 共享对象
class Res{
public String username;
public char sex;
}
// 输入线程对应的类
class InputThread extends Thread{
private Res res;
public InputThread(Res res){
this.res = res;
}
// 重写run()方法
@Override
public void run() {
int count = 0;
while (true){
if (count == 0){
res.username = "hexin";
res.sex = '男';
}else{
res.username = "xiaohong";
res.sex = '女';
}
count = (count + 1) % 2;
}
}
}
// 输出线程对应的类
class OutputThread extends Thread{
@Override
public void run() {
while (true){
System.out.println(res.username + " " + res.sex);
}
}
private Res res;
public OutputThread(Res res){
this.res = res;
}
}
public static void main(String[] args) {
new Thread02().print();
}
public void print(){
// Res为全局共享变量
Res res = new Res();
InputThread inputThread = new InputThread(res);
OutputThread outputThread = new OutputThread(res);
// 启动输入线程和输出线程
inputThread.start();
outputThread.start();
}
}
使用synchronized为代码块加上锁,因为Res是全局共享变量,所以我们可以设置为Res锁,当输入进程拿到Res锁的时候那么读进程就只能够等待直到输出进程释放了锁,当输入进程拿到锁之后那么输出进程就只能够等待直到输入进程释放了锁,最终加上锁之后那么输出结果是正确的:
public class Thread02 {
class Res{
// 共享对象
public String username;
public char sex;
}
// 输入线程
class InputThread extends Thread{
private Res res;
public InputThread(Res res){
this.res = res;
}
// 重写run()方法
@Override
public void run() {
int count = 0;
while (true){
synchronized (res){
if (count == 0){
res.username = "hexin";
res.sex = '男';
}else{
res.username = "xiaohong";
res.sex = '女';
}
count = (count + 1) % 2;
}
}
}
}
class OutputThread extends Thread{
@Override
public void run() {
while (true){
synchronized (res){
System.out.println(res.username + " " + res.sex);
}
}
}
private Res res;
public OutputThread(Res res){
this.res = res;
}
}
public static void main(String[] args) {
new Thread02().print();
}
public void print(){
// Res为全局共享变量
Res res = new Res();
InputThread inputThread = new InputThread(res);
OutputThread outputThread = new OutputThread(res);
// 启动输入线程和输出线程
inputThread.start();
outputThread.start();
}
}
但是上面的代码输出一大段相同的人的名字和性别,我们希望的输出结果是隔一个输出一个人的名字和性别也即交替输出,由于输出线程可能会一直竞争到Res锁所以会一直输出一个人的名字和性别,对于输入线程也是类似的可能会一直获取到Res此时就会一直输入,如何进行改进呢?我们可以在全局变量Res中设置一个布尔值,如果为false则可以输入(输入线程可以操作),为true则可以输出(输出线程可以操作),当flag = false的时候此时是不能够输出的,输出线程拿到当前的Res锁之后需要释放掉当前的锁,此时调用res.wait()方法释放当前获取到的锁并且阻塞当前的线程,让输入线程可以获取到Res锁执行输入操作,当flag = true的时候并且输出线程获取到当前的Res锁此时输出对应的username和sex并且将flag = false,唤醒输入进程使得输入线程可以获取到当前的锁执行输入操作,同理当flag = true的时候输入线程获取到Res锁应该调用res.wait()方法释放当前获取到的锁,并且阻塞当前的线程让输出线程可以输出,当输入线程输入之后需要唤醒输出线程并将flag = true,通过加锁和wait(),notify()方法的配合使得输入线程和输出线程可以交替输出结果:
public class Thread02 {
class Res{
// 共享对象
public String username;
public char sex;
// 标记当前是由输入线程还是输出线程可以操作当前的Res变量
boolean flag = false;
}
// 输入线程
class InputThread extends Thread{
private Res res;
public InputThread(Res res){
this.res = res;
}
// 重写run()方法
@Override
public void run() {
int count = 0;
while (true){
synchronized (res){
// 当flag = true并且竞争到了当前的Res锁此时需要释放当前的锁, 阻塞当前的输入进程, 让输出线程可以获取到当前的锁, 调用res.wait()方法即可
if (res.flag == true){
try {
res.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (count == 0){
res.username = "hexin";
res.sex = '男';
}else{
res.username = "xiaohong";
res.sex = '女';
}
count = (count + 1) % 2;
// 当输入进程输入完之后将标记设置为true, 唤醒输出线程
res.flag = true;
res.notify();
}
}
}
}
class OutputThread extends Thread{
@Override
public void run() {
while (true){
synchronized (res){
if (!res.flag){
// 需要释放掉当前的竞争得到的锁, 并且阻塞当前的线程让给输入线程操作
try {
res.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(res.username + " " + res.sex);
res.flag = false;
// 输出完当前的结果之后唤醒输入线程
res.notify();
}
}
}
private Res res;
public OutputThread(Res res){
this.res = res;
}
}
public static void main(String[] args) {
new Thread02().print();
}
public void print(){
// Res为全局共享变量
Res res = new Res();
InputThread inputThread = new InputThread(res);
OutputThread outputThread = new OutputThread(res);
// 启动输入线程和输出线程
inputThread.start();
outputThread.start();
}
}
四. join()方法的底层原理
join() 底层原理是基于 wait()方法封装的,唤醒的代码在 jvm Hotspot 源码中,当jvm在关闭线程之前会检测先阻塞在 t1 线程对象上的线程,然后执行 notfiyAll(),这样 t2 就被唤醒了,同理线程t3也是也是类似的,通过join()方法就可以实现t1,t2,t3的执行顺序:
public class Thread03 {
public static void main(String[] args) {
// run方法执行完毕 唤醒t2 t2由阻塞转为竞争锁的状态
Thread t1 = new Thread(() ->
System.out.println(Thread.currentThread().getName() + ",线程执行")
, "t1");
// t2需要等待t1执行完毕
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
// t1.wait(); t2调用 t1(this锁).wait() 主动释放this锁, 同时t2线程变为阻塞状态
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ",线程执行");
}
}, "t2");
// t3需要等待t2执行完毕
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ",线程执行");
}
}, "t3");
t1.start();
t2.start();
t3.start();
}
}
五. 多线程七种执行的状态
1. 初始化状态;2. 就绪状态;3. 运行状态;4. 死亡状态;5. 阻塞状态;6. 超时等待;7. 等待状态。start():调用 start()方法会使得该线程开始执行,正确启动线程的方式;wait():调用 wait()方法,进入等待状态,释放资源,让出 cpu,这个方法需要在同步代码块中调用;sleep():调用 sleep()方法,进入超时等待,不释放资源,主动释放 cpu 执行权,休眠一段时间;stop():调用 stop()方法,线程停止,线程不安全,不释放锁导致死锁,过时;join():调用 join()方法,线程是同步,它可以使得线程之间的并行执行变为串行执行;yield():暂停当前正在执行的线程对象,并执行其他线程,让出 cpu 资源可能立刻获得资源执行,yield()的目的是让相同优先级的线程之间能适当的轮转执行;notify():在锁池随机唤醒一个线程,需要在同步代码块中调用;notifyAll():唤醒锁池里所有的线程,需要在同步块中调用;Synchronized 没有获取到锁当前线程变为阻塞状态,如果有线程释放了锁,唤醒正在阻塞没有获取到锁的线程,重新进入到获取锁的状态。
下面是B站视频余胜军的画的关于多线程的七种执行状态
六. sleep()方法防止cpu占用100%
线程运行在cpu上,所以当出现死循环的时候cpu占用会非常高,可以在任务管理器中查看占用的cpu,可以使用Thread.sleep()方法让当前的进程暂时处于休眠的状态,可以防止cpu占用过高:
public class Thread {
public static void main(String[] args) {
new Thread(() -> {
while (true){
try {
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
七. 守护线程和用户线程
java中线程分为两种类型:用户线程和守护线程。通过 Thread.setDaemon(false)设置为用户线程;通过 Thread.setDaemon(true)设置为守护线程。如果不设置次属性,默认为用户线程。
1. 守护线程是依赖于用户线程, 用户线程退出了,守护线程也就会退出,典型的守护线程如垃圾回收线程;
2. 用户线程是独立存在的,不会因为其他用户线程退出而退出。当前有一个主线程,主线程又创建了一个子线程,当主线程执行完毕之后,子线程还没有执行完,那么子线程不会因为主线程执行完而挂掉,当前创建的线程用户线程所以不会因为主线程挂掉而挂掉:
public class Thread04 {
public static void main(String[] args) {
new Thread(() -> {
while (true){
}
}, "子线程").start();
System.out.println("主线程");
}
}
若有一个需求当我们的主线程执行完成之后如何停止子线程呢?只要将设置为守护线程即可:
public class Thread04 {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (true){
}
}, "子线程");
// 设置为守护线程
thread.setDaemon(true);
System.out.println("主线程");
}
}
八. 如何安全停止一个线程
使用interupt()方法进行中断,结合isInterrupted()方法或者是标记位来停止一个线程:
public class Thread05 extends Thread{
@Override
public void run() {
while (true){
try {
System.out.println("1");
Thread.sleep(3000000);
// 中断之后没有执行到, 休眠状态中断之后会抛出一个异常
System.out.println("2");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread05 thread = new Thread05();
thread.start();
Thread.sleep(3000);
System.out.println("中断当前的子线程");
// 中断, 阻塞当前正在运行的线程
thread.interrupt();
}
}
public class Thread05 extends Thread{
@Override
public void run() {
while (true){
if (this.isInterrupted()){
break;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread05 thread = new Thread05();
thread.start();
Thread.sleep(3000);
System.out.println("中断当前的子线程");
// 中断, 阻塞当前正在运行的线程
thread.interrupt();
}
}
九. Lock锁
1. Synchronized 锁与Lock区别
推荐使用Synchronized 锁,因为Synchronized 锁在底层做了优化,特别是锁升级,Synchronized ---属于 jdk 关键字,底层属于c++虚拟机底层实现,Lock 锁底层基于 AQS 实现--- 变为重量级
Synchronized 底层原理---锁的升级过程;Lock锁在使用的过程中需要注意获取锁,释放锁;
2. Lock锁的使用
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
// 获取锁和释放锁由开发人员自己定义
public class Thread06 {
private Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread06 thread06 = new Thread06();
thread06.print1();
Thread.sleep(300);
System.out.println("线程2开始抢锁");
thread06.print2();
}
private void print1(){
new Thread(new Runnable() {
@Override
public void run() {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "获取锁成功");
}catch (Exception e){
e.printStackTrace();
}finally {
// 释放锁, 这样线程2才可以获取到这把锁
lock.unlock();
}
}
}, "t1").start();
}
private void print2(){
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " 1");
lock.lock();
System.out.println(Thread.currentThread().getName() + " 2");
}catch (Exception e){
e.printStackTrace();
}
}
}, "t2").start();
}
}
3. Lock锁的通信(类似于synchronized锁的wait()和notify()方法的思想)
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Thread07 {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public static void main(String[] args) {
Thread07 thread07 = new Thread07();
thread07.cal();
try {
Thread.sleep(3000);
} catch (Exception e) {}
thread07.signal();
}
public void signal() {
try {
lock.lock();
condition.signal();
} catch (Exception e) {
} finally {
lock.unlock();
}
}
public void cal() {
new Thread(new Runnable() {
@Override
public void run() {
// 主动释放锁 同时当前线程变为阻塞状态
try {
lock.lock();
System.out.println("1");
// 注意是await()方法
condition.await();
System.out.println("2");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}).start();
}
}
十. 多线程yield
yield()方法主动释放 cpu 执行权
1. 多线程 yield 会让线程从运行状态进入到就绪状态,让后调度执行其他线程。
2. 具体的实现依赖于底层操作系统的任务调度器,具体的结果还需要由cpu的任务调度器来决定是否让出cpu执行权:
public class Thread08 extends Thread {
public Thread08(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 50; i++) {
if (i == 30) {
System.out.println(Thread.currentThread().getName() + ", 释放cpu执行权");
yield();
}
System.out.println(Thread.currentThread().getName() + "," + i);
}
}
public static void main(String[] args) {
new Thread08("thread1").start();
new Thread08("thread2").start();
}
}
十一. join/wait方法 与 sleep方法之间的区别
1. sleep(long)方法在睡眠时不释放对象锁;
2. join(long)方法先执行另外的一个线程, 在等待的过程中释放对象锁,底层是基于wait()方法封装的;
3. wait(long)方法释放当前的锁资源,并且当前线程也会阻塞,需要在同步代码块中使用(对象锁);
十二. 字节码指令角度分析线程安全问题
从三个角度理解线程安全问题:1. 字节码;2. 上下文切换;3. JMM(java内存模型)可以观察下面的例子,sum是全局变量,两个线程对全局变量进行累加的操作,但是最终的结果小于20000,为什么会小于20000呢?之前是从概念的角度来理解线程安全问题也可以理解一些原因,但是需要从底层来看才知道具体的原因:
public class Thread09 extends Thread {
private static int sum = 0;
@Override
public void run() {
sum();
}
public void sum() {
for (int i = 0; i < 10000; i++) {
sum++;
}
}
public static void main(String[] args) throws InterruptedException {
Thread09 t1 = new Thread09();
Thread09 t2 = new Thread09();
t1.start();
t2.start();
// 哪个线程调用join()方法哪个线程就先让谁先执行, 主线程先让t1, t2线程先执行, 最后再输出结果
t1.join();
t2.join();
System.out.println(sum);
}
}
一般来说,java源代码---->编译成.class文件,我们可以找到java文件编译之后的class文件,在idea运行之后在target目录下找到对应的class文件,在控制台下方找到terminal,将对应的class文件拖到terminal窗口,这样就表示我们正在以cmd命令的形式访问target目录找到对应的class目录,输入命令:javap -p -v Thread09.class,输入这样的命令表示我们对当前的Thread09.class文件做了反汇编的指令:
其中sum()方法对应的sum++汇编指令如下图所示:
getstatic:获取静态变量值 sum,#3表示为常量池中sum的编号
iconst_1:准备一个常量 1
iadd:自增
putstatic:将修改后的值存入静态变量 sum
两个线程在轮流执行的时候会进行上下文的切换,线程1执行sum++的时候可以执行完13行之后就切换到线程2执行,线程2可以可以将所有sum++对应的汇编指令都执行完,将全局变量置为1,此时通过上下文的切换,由于存在程序计数器所以切换到线程1的时候会跳转到第13行执行,将全局变量sum的值又赋值为1,所以实际上是丢失了一次加1的操作,所以最终的结果大部分是小于20000的:
十三. Callable与FutureTask原理分析
当需要线程返回结果的时候可以使用Callable和FutureTask模式,下面的例子中主线程调用子线程异步执行那么需要等待子线程异步执行的结果主线程才可以继续往下执行:
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Thread10 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadCallable threadCallable = new ThreadCallable();
FutureTask<Integer> integerFutureTask = new FutureTask<Integer>(threadCallable);
new Thread(integerFutureTask).start();
// 主线程需要等待子线程的返回结果才可以往下执行
Integer result1 = integerFutureTask.get();
System.out.println(Thread.currentThread().getName());
}
}
import java.util.concurrent.Callable;
public class ThreadCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + "开始执行..");
try {
Thread.sleep(3000);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName() + "返回1");
return 1;
}
}
根据上面的Callable和FutureTask模式的原理我们可以自己手写一个Callable和FutureTask模式的代码,因为new Thread()方法直接传递的FutureTask,说明FutureTask实现了Runnable接口;1. 创建一个自定义的Callable接口,里面写一个返回值为V的call()方法;2. 创建CallableImpl实现接口的call()方法;3. 创建自定义的FutureTask的MyFutureTask类,实现Runnable接口,run()方法中调用Callable中的call()方法,写一个返回值为V的get()方法,因为FutureTask调用get()方法的时候需要等待子线程执行完所以当获取到synchronized锁的时候需要调用wait()方法阻塞,当子线程的run()方法执行完之后通过notify()方法唤醒:
Callable接口:
public interface Callable<V> {
// 返回值为v
V call() throws InterruptedException;
}
Callable接口实现:
public class CallableImpl implements Callable{
@Override
public Integer call() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "在执行耗时的代码");
// 当前子线程阻塞3s
Thread.sleep(3000);
return 1;
}
}
MyFutureTask实现:
package com.example.demo.controller;
public class MyFutureTask<V> implements Runnable{
private Callable<V> callable;
private Object lock = new Object();
private V res;
// 构造方法传入Callable接口
public MyFutureTask(Callable<V> callable){
this.callable = callable;
}
// 执行run()方法
@Override
public void run() {
try {
res = callable.call();
// 唤醒当前的主线程
synchronized (lock){
lock.notify();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 主线程调用当前的get()方法
public V get() throws InterruptedException {
synchronized (lock){
lock.wait();
}
return res;
}
}
测试类:
public class Thread11 {
public static void main(String[] args) throws InterruptedException {
Callable callable = new CallableImpl();
MyFutureTask<Integer> myFutureTask = new MyFutureTask<Integer>(callable);
new Thread(myFutureTask).start();
Integer res = myFutureTask.get();
System.out.println("获取到结果为: " + res);
}
}
十四. LockSupport
package com.example.demo.controller;
import java.util.concurrent.locks.LockSupport;
public class Thread12 {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 1");
// 当前的子线程阻塞
LockSupport.park();
System.out.println(Thread.currentThread().getName() + " 2");
}
});
t1.start();
try{
System.out.println(Thread.currentThread().getName() + " 3");
Thread.sleep(3000);
LockSupport.unpark(t1);
}catch (Exception e){
}
}
}
十五. 日志框架
大部分的日志框架都是基于多线程的技术,我们根据之前学习到的多线程技术手写一个日志框架,写日志为什么会有多线程呢?如果不使用多线程来解决会存在很大的一个bug,比如不使用多线程的情况下日志写入到磁盘的步骤:1. 获取打印的日志;2. 将日志写入到磁盘中;有可能在调用业务方法之前多次调用将日志写入到磁盘中的操作方法,如果直接使用单线程来解决那么肯定会影响到整个系统的吞吐量,所以将所有的日志全部执行完再执行业务代码肯定是不合理的,所以将日志写入到磁盘中是一个异步过程,log.info()的本质是先将日志缓存到队列中,使用单独的线程从缓存队列中获取日志,将日志写入到磁盘中,因为是异步将日志写入到磁盘中所以一个很好的好处是不会影响到整体的接口通信量,写日志是最花时间的,因为需要用户态与内核态之间的切换。