多线程分时系统线程安全问题-synchronize
问题
多线程分时系统是存在线程安全问题的,如下例子:
两个线程分别对同一个变量(初始值 = 0)做循环自增和自减操作各50000次,观察结果,并不等于初始值。
public class 分时系统线程安全问题 {
static int a = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
a++;
}
},"t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
a--;
}
},"t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(a);
}
}
出现以上问题的原因就是,看似单个的Java语句(a++、a--)会被操作系统拆分为多几条指令。自增自减并不是原子操作。在多线程的情况下下面的8条指令是会交错执行的,必然导致结果!=预期
a++:
getstatic i //获取静态常量的值
iconst_1 //准备常量1
iadd //自增
putstatic //讲修改后的值写入静态变量i
a--:
getstatic i //获取静态常量的值
iconst_1 //准备常量1
isub //自减
putstatic //讲修改后的值写入静态变量i
临界区 Critical Section
一个程序运行多个线程本身没有问题,问题出在多个线程访问了共享资源,多个读写共享资源其实也没问题,但是多个线程在访问共享资源是出现了指令交错就会有问题。
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
static int counter = 0;
static void increment(){
//临界区
counter++;
}
static void decrement(){
//临界区
counter--;
}
竞态条件 Race condition
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
解决问题
1、应用之互斥
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
阻塞式的解决方案:synchronized,Lock
非阻塞式的解决方案:原子变量
synchronized
语法
synchronize(对象){
//临界区
}
static int a = 0;
/**
* 对象锁
*/
static Object lock = new Object();
/**
* 线程安全
*/
public static void test2() throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
synchronized (lock) {
a++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
synchronized (lock) {
a--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(a);
}
synchronize利用对象锁保证了临界区内的代码块的原子性(不可分割)。
过程分析:
思考:
1、synchronize放在for循环外面能保证结果正确吗?(--原子性)
synchronized (lock) {
for (int i = 0; i < 50000; i++) {
a++;//a--;
}
}
答案:可以的synchronize关键字包含的代码块为一个整体不可分割。
2、t1里面synchronize(lock1)、t2里面synchronize(lock2)能保证结果正确吗?(--锁对象)
答案:不能,加锁要是同一个对象锁、保护共享资源,必须保证同一个对象锁。
3、t1加锁 synchronize(lock),t2不加锁能保证结果正确吗?(--锁对象)
答案:不能,同问题2.
面向对象的优化:
public class 分时系统线程安全问题2 {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
room.increment();
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
room.decrement();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(room.getA());
}
}
class Room {
private int a = 0;
public void increment() {
synchronized (this) {
a++;
}
}
public void decrement() {
synchronized (this) {
a--;
}
}
public int getA() {
return a;
}
}
synchronize的使用位置:
1、加在普通法放上。(所的是this对象)
class Test{
public synchronized void test() {
}
}
//等价于
class Test{
public void test() {
synchronized(this) {
}
}
}
2、加在静态方法上(锁的是class对象,调用者公用一把锁)
class Test{
public synchronized static void test() {
}
}
//等价于
class Test{
public static void test() {
synchronized(Test.class) {
}
}
}
本文来自博客园,作者:iyandongsheng,转载请注明原文链接:https://www.cnblogs.com/ieas/p/16735928.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现