线程安全问题:简单来说,就是在多线程的调度下,导致出现了一些随机性,随机性使代码出现 了一些bug =>线程不安全

造成线程不安全的原因有哪些呢?

1)抢占式执行,调度过程随机(也是万恶之源,无法解决)

2)多个线程同时修改同一个变量(可以适当调整代码结构,避免这种情况)

3)针对变量的操作,不是原子的(加锁,synchronized)

4)内存可见性,一个线程频繁读,一个线程写(使用volatile)

5)指令重排序(使用synchronized)

解决方法:

1)使用synchronized关键字以及synchronized的基本使用

synchronized的本质操作,是修改了Object对象中的“对象头”里面的一个标记,只有当两个线程同时针对一个对象加锁,才会产生竞争

把synchronized加到普通的方法上,也就相当于把锁对象指定为this了

线程安全list java 线程安全问题_线程安全list java

把synchronized加到代码块上,就需要手动锁定,锁对象是啥(针对那个对象加锁) 

线程安全list java 线程安全问题_加锁_02

把synchronized加到静态方法上,所谓的“静态方法”更严谨的叫法,应该叫做“类方法”,普通的方法,更严谨的叫法,叫做”实例方法“ ,针对类对象加锁如下:

线程安全list java 线程安全问题_加锁_03

类对象就是运行程序的时候,.class文件被加载到JVM内存中的模样 

2)synchronized的特性

1.互斥
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到
同一个对象 synchronized 就会阻塞等待.
进入 synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁

2.刷新内存~synchronized 的工作过程
1. 获得互斥锁
2. 从主内存拷贝变量的最新副本到工作的内存
3. 执行代码
4. 将更改后的共享变量的值刷新到主内存
5. 释放互斥锁

3.可重入

可重入直观上来讲,就是同一个线程针对同一个锁,连续加锁了两次,如果出现了死锁,就是不可重入,如果不会死锁,就是可重入的~~

死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止

死锁的四个必要条件:

1)互斥使用~ 一个锁被一个线程占用了之后,其他线程占用不了(锁的本质,保证原子性)

2)不可抢占~ 一个锁被一个线程占用了之后,其他线程不能把这个锁给抢走

3)请求与保持~ 当一个线程占据了多把锁之后,除非显式的释放锁,否则这些锁是始终都是被该线程所持有的

4)环路等待,等待关系(为避免环路等待,只需要约定好,针对多把锁加锁的时候,有固定的顺序即可) 

2)volatile关键字

volatile保证内存可见性,禁止编译器优化

volatile只是处理一个线程读,一个线程写的情况

volatile不保证原子性,也不会引起线程阻塞

3)wait和notify

由于线程之间是抢占式执行的,因此线程之间执行的顺序难以预知,因此我们需要合理的协调多个线程之间的执行先后顺序

完成这个协调工作, 主要涉及到三个方法
wait() / wait(long timeout): 让当前线程进入等待状态.
notify() / notifyAll(): 唤醒在当前对象上等待的线程.
注意: wait, notify, notifyAll 都是 Object 类的方法.

1,wait()方法

wait 做的事情:

public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object) {
System.out.println("等待中");
object.wait();
System.out.println("等待结束");
}
}

1.使当前执行代码的线程进行等待. (把线程放到等待队列中)

2.释放当前的锁

3.满足一定条件时被唤醒,重新尝试获取这个锁
注意:wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.

wait 结束等待的条件:

1.其他线程调用该对象的 notify 方法.

2.wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).

3.其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常

2,notify()方法:

notify 方法是唤醒等待的线程.

static class WaitTask implements Runnable {
private Object locker;
public WaitTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (locker) {
while (true) {
try {
System.out.println("wait 开始");
locker.wait();
System.out.println("wait 结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
static class NotifyTask implements Runnable {
private Object locker;
public NotifyTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (locker) {
System.out.println("notify 开始");
locker.notify();
System.out.println("notify 结束");
}
}
}
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(new WaitTask(locker));
Thread t2 = new Thread(new NotifyTask(locker));
t1.start();
Thread.sleep(1000);
t2.start();
}

1.方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的
其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。

2.如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 "先来后到")

3.在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行
完,也就是退出同步代码块之后才会释放对象锁。

notifyAll()方法

notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程

wait和sleep的对比(简单了解即可)
 

wait用于线程之间的通信的,sleep是让线程阻塞一段时间

唯一的相同点就是都可以让线程放弃执行一段时间.

1. wait 需要搭配 synchronized 使用. sleep 不需要.

2. wait 是 Object 的方法 sleep 是 Thread 的静态方法