如何理解线程安全:
多线程并发执行的时候,有时候会触发一些“bug”,虽然代码能够执行,线程也在工作,但是过程和结果都不符合我们的开发时的预期,所以我们将此类线程称之为“线程安全问题”。
例如:在多个线程并发的时候,操作系统对多线程的调度特性会导致结果存在偶然性,这个偶然性可能很小,但也不小(eg:假设偶然性为0.1‰,并发的线程有200_000个,那出现的偶然性结果也会有200个,也就是每20w个用户就会影响到200个用户体验,如果是更大体量的那就影响更大了)
代码实例:如图,我们的实例预期结果本来应该是5000+5000 =10000
多次运行结果都不一样,为什么不一样呢? 问题的关键就在于——并发执行会有偶然性,如果是串行执行那么就不会有问题~
进一步来体会并发执行的过程:
从内核的时间轴来看线程代码的执行
通过上述的这些问题,我们再细致的说说线程不安全的原因:
1、内核对线程调度的随机性(非人力能干涉的不可控因素~)
2、当前代码有多个线程对变量进行操作:(变量也可以是硬盘上的数据/网络上的数据)
①多个线程修改同一个变量——>不安全,代码在执行时如果被其他的线程抢占执行,那么结果很有可能就是错的~
②多个线程读取同一个变量——>没事儿~只读不改,就相当于每个线程在内存中拷贝一份这个变量过来~
③多个线程修改不同的变量——>没事儿~你改你的,我改我的,井水不犯河水~
④单个线程修改同一个变量——>没事儿~每改一次就从内存拿出来一次,改完就放回内存去~
3、线程针对变量的修改不是原子性的(如果线程不是抢占式执行,那么没有原子性也没有关系~)
什么是原子性?
所谓“原子性”,就是不可拆分的最小单位,也就是说,当对一个变量的修改是执行一个最小量级的命令——CPU指令,则称这个操作是具有原子性的。
拿上述count++举例,count++这个语句在CPU内核中是分为三步实现:在内存中将count拿出来,在CPU寄存器上进行count+1,将计算结果放回内存。这一句简单的语句需要三个指令来实现,那么对count这个变量的修改就不具备原子性~
可见性:
多个线程在修改同一个变量的时候,能够让其他的线程同时看见这个变量。
JMM里的模拟内存:
每一个线程都有各自的一块工作内存,在线程创造的时候申请分配工作内存,线程销毁的时候释放工作内存。对一个变量进行修改不会直接在主内存内对变量进行修改,而是在主内存中拷贝一份到线程的工作内存中进行修改,然后进行数据更新后再写入主内存~
内存的可见性:
众所周知,每个线程都会申请一块属于自己的工作内存,对于数据的修改,线程总是先从主内存拷贝一份到工作内存,然后在寄存器上进行操作之后再将数据放回内存。
当有多个线程对同一个变量操作时,如何让两个线程的操作都是有效的?这时候就涉及到内存的可见性~
例:现有两个线程,t1线程只有在线程t2通知之后才会停下,而我们利用控制台输入一个非0的整数来控制t2通知t1
static class Counter {
public int count = 0;
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(()->{
while(counter.count == 0)
{
}
System.out.println(Thread.currentThread().getName()+"接到通知,马上停下来...");
},"t1");
Thread t2 = new Thread(()->{
Scanner sc = new Scanner(System.in);
System.out.println(Thread.currentThread().getName()+"发出停止通知...");
counter.count = sc.nextInt();
},"t2");
t1.start();
t2.start();
}
可是结果却是t1没有收到t2的通知
为什么会这样?这是因为t2将修改之后的变量刷新到内存,但是这个结果没有在t1中同步刷新,所以就产生了上述的结果
如何解决这一点?用volatile修饰count即可~
拿上面的例子思考一下,如何避免获得上述这种抢占式执行的结果?
①要想避免这种抢占式执行产生的结果,最好的做法就是给线程上锁
②当两个线程执行过程要对同一个变量进行修改的时候,修改的那段代码可以加上同一个锁,这样就会阻塞其中一个线程让其等待,等锁内的线程执行完工作内容再接着执行另一个线程
synchronized关键字:
对于多个线程针对同一个对象,我们如果想要保证程序的原子性,那么就得给这个对象加一个锁,而synchronized就是干这件事的~
进入synchronized(){}的作用域中表示加锁,执行完作用域中的代码就进行解锁。
如果synchronized针对的是多个对象,那么就不会产生锁竞争,也就不会出现阻塞等待,线程各自干各自的活儿。
public class demo2 {
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Object locker2 = new Object();
Thread t1 = new Thread(()->{
System.out.println(Thread.currentThread().getName()+"加锁前....");
synchronized (locker)
{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(Thread.currentThread().getName()+"解锁后....");
},"线程1");
Thread t2 = new Thread(()->{
System.out.println(Thread.currentThread().getName()+"加锁前....");
synchronized (locker2)
{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(Thread.currentThread().getName()+"解锁后....");
},"线程2");
t1.start();
t2.start();
t1.join();
t1.join();
}
}
结果就是两个线程是并发执行的,各干各的,没有产生阻塞等待~
synchronized具有不可抢占性——即如果有人已经持有这把锁,那么在这把锁释放之前其他的线程是拿不到的。
用实例体会一下抢锁的过程:
public class demo3 {
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(()->{
synchronized (locker)
{
System.out.println(Thread.currentThread().getName()+"抢到锁进行加锁....");
}
},"线程1");
Thread t2 = new Thread(()->{
synchronized (locker)
{
System.out.println(Thread.currentThread().getName()+"抢到锁进行加锁....");
}
},"线程2");
Thread t3 = new Thread(()->{
synchronized (locker)
{
System.out.println(Thread.currentThread().getName()+"抢到锁进行加锁....");
}
},"线程3");
t1.start();
t2.start();
t3.start();
}
}
通过结果,我们可以明白——针对同一个对象进行加锁,谁能先拿到锁是随机的。但是,为什么这里的线程1能一直先拿到锁?这是因为后面t2、t3线程启动需要时间,在这短短的启动时间里,t1可以先获得锁~
在其他的语言中,加锁操作并不是一个synchronized就能搞定,而是线程{ lock(); 其他代码.....; unlock(); }~有时候往往会把unlock()给忘了,这时候就出错了,而synchronized在封装过程中这些都帮我们写好了,我们直接用没有后顾之忧~
wait()和notify():
调用wait()方法干的事:
①让当前线程进行阻塞
②释放当前的锁
③满足一定条件被唤醒,重新尝试获取这个锁(不一定唤醒了就能获取的到,依旧是和其他线程抢占执行)
class WaitTask implements Runnable{
private Object locker = new Object();
public WaitTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (locker)
{
try {
System.out.println("开始阻塞...");
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
class NotifyTask implements Runnable{
private Object locker = new Object();
public NotifyTask(Object locker)
{
this.locker = locker;
}
@Override
public void run() {
synchronized (locker)
{
locker.notify();
System.out.println("线程已经被唤醒...");
}
}
}
public class demo1 {
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(new WaitTask(locker));
/* Thread t2 = new Thread(new WaitTask(locker));
Thread t3 = new Thread(new WaitTask(locker));*/
Thread t4 = new Thread(new NotifyTask(locker));
t1.start();
// t2.start();
// t3.start();
Thread.sleep(5000);
t4.start();
}
}
唤醒线程的方法:①调用该对象的notify()方法;②wait()等待超时;③其他线程调用Interrupted方法,抛出InterruptedExption异常。
notify()方法是唤醒等待中的线程,当有多个线程处于wait()时,由线程调度器随机挑选一个进行唤醒(依旧是没有先到先得的原则)
wait()要搭配synchronized一起使用,不然就会抛异常
当线程调用wait()之后需要调用notify()来唤醒线程,不然就是让程序死等
notify()一次只能唤醒一个线程,而notifyAll()能够一次性唤醒所有线程
一次性唤醒所有等待的线程之后依旧是抢占式执行,依旧有先后执行顺序
区分wait()和sleep():
wait()是用于线程之间的通信,而sleep()只是单纯地让线程阻塞一段时间
1.wait()需要和synchronized搭配使用,而sleep不需要
2.wait()是Object类的方法,而sleep()是Thread类的静态方法