一、线程安全
在多线程编程中,由于调度器的随机调度(抢占式执行)导致程序的执行有多种可能,但其中的某种可能会导致代码出现bug由此引发“线程安全问题”
class counter{
public int count = 0;
public void increase(){
count++;
}
}
public class test2 {
public static void main(String[] args) throws InterruptedException {
counter counter = new counter();
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count : "+counter.count);
}
}
在上述代码中通过多线程实现对count变量两次自增到5w的操作,按照理论来说最终打印的结果应是10w,但其实结果会在5w到10w之间,此处就是典型的由于线程并发执行而产生的bug!!!
上述的bug从硬件的角度来说首先需要cpu从内存中读取数据,完成自增运算,再将结果返回到内存中。在"count++"中对应三个步骤:
- 第一步:cpu从内存中读取数据:load
- 第二步:在cpu的寄存器中完成加法运算:add
- 第三步:将寄存器的数据返回到内存中:save
内存中的运行图:
上图中,线程一和线程二各自对应一个cpu,按照执行顺序来说,线程一会先从内存中读取数据到cpu中进行加法运算,完成后会将数据重新返回到内存中,之后,线程二会重复线程一的步骤,完成上述步骤后count已经自增为2。
但是由于调度系统具有随机性,因此上述代码的执行顺序只是无数可能顺序中的一种:::
上图只是列举了多线程运行的部分情况:
只有当线程一完整执行完三个操作之后再执行另外一个线程的操作时,count才能完成一次自增;而当发生图中的执行顺序时,由于线程一从读取到数据到完成数据的自增到返回数据到内存中,在执行这一系列的操作中,线程二直接插入线程一的操作中,导致读取到没有被线程一加载完成的数据,因此最终返回的数据没有成功完成自增。
1.1、线程不安全的原因
- 操作系统的随机调度/抢占式执行【不可避免】
- 多个线程修改同一个变量【部分可以避免】
- 修改操作不是原子的【可以避免】
- 内存可见性引起的安全问题【优化产生】
- 指令重排序【优化产生】
什么是“内存可见性”???
内存里的某些操作使得数据被改了但是线程没有及时被发现。
什么是“指令重排序”???
言简意赅的说就是一种优化,对于代码中不同的指令,对这些指令进行重排序,从而达到一种优化的效果。
1.2、如何解决线程不安全
使用synchronized对程序进行加锁:::
synchronized具有互斥的属性,当一个对象已经synchronized时,此时如果另一个线程也执行到synchronized时,就会发生“阻塞等待”
在上述代码中,如果多个线程去调用increase的话,(synchronized实质上就是针对counter来加锁)如果其中的一个线程获取到锁,那么另一个线程就要阻塞等待;但是,如果多个线程是对不同的对象进行加锁操作,就没有阻塞等待。总而言之,在使用synchronized的时候要明确对哪个对象进行加锁,当多个线程对同一个对象进行加锁操作的时候会发生阻塞等待,如果多个线程加锁的对象也各不相同就不涉及阻塞等待。
拓展:在使用加锁操作的时候,在一定程度上会降低代码的高性能,例如上述代码中对t1加锁之后,t2再尝试加锁就会进入阻塞状态(blocked),因此t1和t2就不是并发运行。
运行结果分别为:
第一种:t1开始 第二种:t1开始
t1结束 t2开始
t2开始 t1结束
t2结束 t2结束
言简意赅的说:
当t1线程和t2线程调用的对象不一致时,t1和t2是并发执行; 当t1线程和t2线程调用的对象一样时,t2会等待t1接除锁后再执行(阻塞等待)
二、wait和notify
wait的作用:
- 使当前执行代码的线程进行等待;
- 释放当前线程的锁;
- 当满足条件时会被唤醒,重新尝试获取锁;
wait结束等待的条件:
- 其他的线程调用这个对象的notify方法;
- wait达到等待的时间;
- 其他线程调用等待线程的interrupt方法,导致wait抛出interruptexception异常
上述代码中,当代码执行到object1.wait时,线程一会释放锁,从而线程二就会对object1进行加锁操作,当线程二的代码执行到object1.notify时,线程二并不会马上就释放锁,而是要等到线程二的代码执行完才会释放锁,此时线程一会尝试对object1进行后续操作。
注意:wait和notify操作的要是同一个对象才会有作用,否则没有效果;
notifyAll可以同时唤醒所有的线程,但是这些线程需要竞争。
三、单线程案例
3.1、单例模式
什么是设计模式???什么是单例模式???
设计模式就好比象棋中的棋谱,针对不同的开发场景而总结出的固定的套路,按照这个套路来编写代码,实现业务需求;而单例模式能保证某个类在程序中只存在唯一的一份,不会创建出多个类。
单例模式:同时又分为“饿汉”和“懒汉”两种;
饿汉模式:类加载的同时也创建实例
class Singleton{
private static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
private Singleton(){}
}
懒汉模式:类加载的时不创建实例,第一次使用的时候才创建实例
class SingletonLazy{
private static volatile SingletonLazy instance = null;
public static SingletonLazy getInstance(){
if(instance == null){
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy(){}
}
在执行上述代码时,需要考虑线程安全问题:在第一次调用getInstance方法时,里面会有“读”和“写”的问题,为了实现步骤的”原子性“,对线程进行加锁操作,保证“读”和“写”的操作在一起执行。但是当对线程加锁之后,后续在调用getInstance方法时会重复加锁操作,因此会降低代码的执行效率,所以在外围再套上一个if判断。
class SingletonLazy{
private static volatile SingletonLazy instance = null;
public static SingletonLazy getInstance(){
if(instance == null){
synchronized (SingletonLazy.class){
if(instance == null){
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy(){
}
}
如何理解”双重if判断“???
在多线程环境下执行时,多线程通过外层的if判断instance是否已经完后初始化,如果初始化完成,则直接返回instance;当instance没有初始化完成时(即通过外层的if判断语句得知),多个线程此时会竞争这一把锁,此时线程一拿到锁并开始再次确认实例是否已经创建出来,如果没有创建的话会创建实例,实例创建完成后,此时线程一就会释放锁,然后线程二拿到锁并开始通过外层if判断是否已经创建实例,当已经创建实例时就不会再拿锁,因此就会提高代码的执行效率。
3.2、阻塞式队列
什么是“阻塞式队列”???如何理解“阻塞式队列”???“阻塞式队列”有什么用???
“阻塞式队列”:“阻塞”顾名思义就是让程序代码停下来等待,当程序队列已经满的时候停下来,当程序队列为空的时候也会停下来——这就是“阻塞”;其中“生产者消费者模型”最为常见,“阻塞式队列”可以提高“线程安全”,从本质上来说“阻塞式队列”更改了线程的状态,让线程的PCB在内核中暂时不参与调度;
使用“阻塞式队列”优点:
- 可以“解耦合”;
- 使用“生产消费者模型”能够做到“削峰填谷”,提高系统抗风险能力。
Java系统中自带的“阻塞式队列”代码:
自己模拟实现“阻塞式队列”:
class MyBlockingQueue{
//设置数组
private int[] array = new int[1000];
//数组的首元素
private int beg = 0;
//数组的末元素
private int end = 0;
//数组的总元素
volatile private int sum = 0;
//填入数据
public void put(int value) throws InterruptedException {
synchronized (this){
//如果队列已满,则直接返回——>则阻塞——>当队列数据被拿走时再唤醒
while (sum == array.length){
this.wait();
}
//如果队列没满,则填入数据
array[end] = value;
end++;
//如果填入数据后恰好填满,则将end赋为 0
if(end == array.length){
end = 0;
}
sum++;
this.notify();
}
}
//拿走数据
public Integer take() throws InterruptedException {
int tmp = 0;
synchronized (this){
//没有数据直接返回——>此时要发生阻塞——>当填入数据时再唤醒
while (sum == 0){
this.wait();
}
//如果队列有数据,则直接拿走数据
tmp = array[beg];
beg++;
//如果队列数据已满,则将其赋为 0 ;
if(beg == array.length){
beg = 0;
}
sum--;
this.notify();
}
return tmp;
}
}
public class test2 {
public static void main(String[] args) throws InterruptedException{
MyBlockingQueue queue = new MyBlockingQueue();
Thread customer = new Thread(()->{
while (true){
int value = 0;
try {
value = queue.take();
System.out.println("拿到数据: " + value);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
customer.start();
Thread producer = new Thread(()->{
int value = 0;
while (true){
try {
queue.put(value);
System.out.println("填入数据" + value);
value++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producer.start();
}
}
在这里,当队列为空时要阻塞·,当队列不为空(填入数据时)就唤醒;当队列为满时要阻塞,当队列不为满(数据被拿走)就唤醒;而对于数组的操作:当填入一个数据时,在刚开始时beg和end均在同一处,每填入一个数据end就往后走一个位置(beg不动);当取出数据时,通过beg来取数据,每取出一个数据,beg就往后走一个位置;当beg或者end走到的位置与数组的长度相同时就会被赋为0,即重新在开始的位置。
3.3、定时器
import java.util.Timer;
import java.util.TimerTask;
public class test1 {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello world");
}
},3000);
}
}
通过代码来实现定时器:
import java.util.concurrent.PriorityBlockingQueue;
//描述任务
class MyTask implements Comparable<MyTask>{
//执行时间
private long after;
//任务内容
private Runnable runnable;
//构造方法
public MyTask(Runnable runnable,long after){
this.after = System.currentTimeMillis() + after;
this.runnable = runnable;
}
//执行任务
public void run(){
runnable.run();
}
//获取时间
public long getAfter() {
return after;
}
@Override
public int compareTo(MyTask o) {
return (int) (this.after-o.after);
}
}
//设置定时器
class MyTimer{
//创建锁对象
private Object locker = new Object();
//使用优先级队列来保存任务
private PriorityBlockingQueue<MyTask> queue= new PriorityBlockingQueue<>();
//创建任务:
public void schedule(Runnable runnable,long after){
MyTask myTask = new MyTask(runnable,after);
synchronized (locker){
queue.put(myTask);
locker.notify();
}
}
public MyTimer(){
Thread thread = new Thread(()->{
while (true){
try {
synchronized (locker){
while (queue.isEmpty()){
locker.wait();
}
MyTask myTask = queue.take();
long curtime = System.currentTimeMillis();
//当执行时间还没到时,返回原队列中
if(myTask.getAfter() > curtime){
queue.put(myTask);
locker.wait(myTask.getAfter()-curtime);
}else{
//当执行时间到了,则执行任务
myTask.run();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
}
}
public class test2 {
public static void main(String[] args) {
MyTimer myTimer = new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("111");
}
},5000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("222");
}
},3000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("333");
}
},1000);
}
}
是
3.4、线程池
什么是“线程池”???“线程池”的意义???
在编程时,进程和线程都可以做到并发编程(线程要更方便),但是为了进一步优化,就产生了“线程池”:线程池就是把线程创建好之后放在一个池子里面,需要用时就直接取,不用时再放回去。
为什么线程池的方法要比从系统里面创建线程要快???
从池子里取是“纯用户态操作”,而从系统里面创建则是涉及到“内核态操作”,通常认为,“纯用户态操作”要比“内核态操作”更高效
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class test3 {
public static void main(String[] args) {
//创建线程池:
ExecutorService threadPool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
threadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello world");
}
});
}
}
}
通过代码实现线程池
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
//代码实现线程池;先创建一个任务队列,再往线程池里面插入任务
class MyThreadPool{
//创建任务队列
private BlockingQueue<Runnable> queue = new LinkedBlockingDeque<>();
//往线程池里面插入任务
public void put(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
//创建线程池:
public MyThreadPool(int n){
for (int i = 0; i < n; i++) {
Thread thread = new Thread(()->{
while (!Thread.interrupted()){
try {
Runnable runnable = queue.take();
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
});
thread.start();
}
}
}
public class test4 {
public static void main(String[] args) throws InterruptedException {
MyThreadPool myThreadPool = new MyThreadPool(10);
for (int i = 0; i < 100; i++) {
myThreadPool.put(new Runnable() {
@Override
public void run() {
System.out.println("hello world");
}
});
}
}
}
在使用代码自己实现线程池时,需要先创建一个“任务队列”,这是把将要执行的任务都存放在这个队列中;再自定义一个方法,这个方法用于把任务放在“任务队列”中;此时创建MyThreadPool构造方法,在这个构造方法里面通过for循环的方式来创建多个线程,在for循环内部,通过take方法将“任务队列”上的任务拿下来赋值给Runnable类型的变量,通过这个变量调用run方法执行任务。因此在循环的同时,既创建了多个线程又执行了任务。