本文章是根据黑马JUC课程编写,记录的笔记
在平常开发中,很多时候都会遇到共享数据的问题,比如售票,库存。那么如何就会引出一个疑问,如何保证数据的安全性呢(就是数据共享的问题)!
static Integer num = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
num++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
num--;
}
}, "t2");
t1.start();
t2.start();
t1.join();// 使其他线程等待t1执行完成
t2.join();// 使其他线程等待t2执行完成
log.info("num:{}", num);
}
执行多次,会发现,每次结果不相同,正常情况下,他们的执行结果会是0。 为什么结果不是0 首先我们得了num++ 会产生什么字节码 num++ getstatic i // 获取静态变量i的值 iconst_1 // 准备常量1 iadd // 自增 putstatic i // 将修改后的值存入静态变量i num-- getstatic i // 获取静态变量i的值 iconst_1 // 准备常量1 isub // 自减 putstatic i // 将修改后的值存入静态变量i 简单来说,就是当我们
t1线程刚对num进行自增操作时
,此时线程进行了程序上下文切换
(简单说就是cpu给他的时间用完了
,进入Rnnable 可运行状态
),然而下一次上下文切换,并没有给到t1,给到t2
。t1还未对常量池中的num赋值
,也就是说,t1这次的自增没有成功
,假如t2成功,num的值就发生了错乱。这也就是num不为0的原因
例如,下面代码中的临界区:
static int counter = 0;
static void increment()
// 临界区
{
counter++;
}
static void decrement()
// 临界区
{
counter--;
}
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
我们采用synchronized对象锁的方式来解决,其他解决方案后续会有。
即俗称的【对象锁】,它采用互斥的方式让
同一 时刻至多只有一个线程能持有【对象锁】
,其它线程再想获取这个【对象锁】时就会阻塞
住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。
synchronized(对象) // 线程1, 线程2(blocked)
{
临界区
}
把上面案例的临界区用 synchronized 包裹
static int counter = 0;
static final Object room = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (lock) {
num++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (lock) {
num++;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.info("num:{}", num);
}
synchronized 实际是用对象锁保证了临界区内代码的
原子性
,临界区内的代码对外是不可分割的,不会被线程切换所打断。
会被阻塞
),然后继续上下文切换,直到切换到拥有锁的线程,也就是我们线程2
.几个小提问
还是上面那个问题,平常开发中,我们更多的是对 对象的某个属性进行操作,所有我们采用
面向对象的方式,解决这个问题。
@Slf4j
public class C1_线程安全面向对象 {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
// 创建两个线程
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.add();
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.sub();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.info("num:{}", room.get());
}
}
// 被操作的对象
class Room{
int num = 0;
// 自加
public void add(){
synchronized (this){
num++;
}
}
// 自减
public void sub(){
synchronized (this){
num--;
}
}
// 获取
public int get(){
synchronized (this){
return num;
}
}
}
可以执行测试,效果是一样的。
成员变量和静态变量是否线程安全?
局部变量是否线程安全?
局部变量是线程安全的 但局部变量
引用的对象
则未必
先看一个成员变量的例子
class ThreadUnsafe {
ArrayList<String> list = new ArrayList<>();
public void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++) {
// { 临界区, 会产生竞态条件
method2();
method3();
}
}
private void method2() {
list.add("1");
}
private void method3() {
list.remove(0);
}
// 线程的数量
static final int THREAD_NUMBER = 2;
// 执行的次数
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadUnsafe test = new ThreadUnsafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + i).start();
}
}
}
其中一种情况是,如果线程2 还未 add,线程1 remove 就会报错:
示例图
public void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
// { 临界区, 会产生竞态条件
method2();
method3();
}
}
private void method2() {
list.add("1");
}
private void method3() {
list.remove(0);
}
采用
局部变量
,就不会出现上面问题了。可以看到,每个线程都只是对自己的局部变量进行操作
。
示例图
方法访问修饰符带来的思考,
如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题? 情况1:有其它线程调用 method2 和 method3 情况2:在 情况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,即
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
// 形成新的临界区
method2(list);
method3(list);
}
}
private void method2(ArrayList<String> list) {
list.add("1");
}
private void method3(ArrayList<String> list) {
list.remove(0);
}
}
class ThreadSafeSubClass extends ThreadSafe{
@Override
public void method3(ArrayList<String> list) {
new Thread(() -> {
list.remove(0);
}).start();
}
}
执行,还是会存在
线程安全问题
,在for循环中,又会形成新的临界区,因为子类重写了method3,创建了线程,然而我们不能限制子类的行为。
闭合原则:String
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为
Hashtable table = new Hashtable();
new Thread(()->{
table.put("key", "value1");
}).start();
new Thread(()->{
table.put("key", "value2");
}).start();
Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
table.put("key", value);
}
别灰心,你肯定打错了,我也打错了。给个提示,这些线程安全类的方法,单个是线程安全的,那么多个组合起立还是不是呢。
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的 有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?