线程同步问题
一.引言
首先一点,我们要明确的是线程同步操作,是为了解决共享资源的冲突问题,保证数据的统一性。
举个例子:
对于某一个数组资源:
用户A想要读取其中的下标为0的数据,同时,
用户B想要修改其中的下标为0的数据.
那么最后的结果很难说清楚。。。
这时候就要用到我们的线程同步操作了。
二、案例带入
首先,我们来看这样一个售票案例:
例如马上要上映的新海诚新电影《铃芽户缔》要开始售票了。
A、B、C三个平台总共接到售票100张的任务,
这一百张票是三个平台一共要售出的票数,在此可以看成是共享资源。
那么我们首先来看一个如果没加上线程同步,售票结果会是怎么样,代码如下:
线程任务部分:
public class MyThread implements Runnable {
//定义总票数100,为共享资源
private static int ticket = 100;
@Override
public void run() {
Thread thread = Thread.currentThread();
while (true) {
if (ticket > 0) {
System.out.println(thread.getName() + "售出票号为" + ticket + "的票");
ticket -= 1;
} else {
//售罄,跳出循环
System.out.println(thread.getName() + "售罄");
break;
}
}
}
}
测试:
public class Demo1 {
public static void main(String[] args) {
Thread t1 = new Thread(new MyThread(), "A");
Thread t2 = new Thread(new MyThread(), "B");
Thread t3 = new Thread(new MyThread(), "C");
t1.start();
t2.start();
t3.start();
}
}
测试结果:
B售出票号为100的票
B售出票号为99的票
B售出票号为98的票
B售出票号为97的票
B售出票号为96的票
B售出票号为95的票
C售出票号为100的票
C售出票号为93的票
C售出票号为92的票
A售出票号为100的票
A售出票号为90的票
C售出票号为91的票
B售出票号为94的票
可以看出,三个线程之间对资源的抢占,让电影票的售票及其不合理,这就需要线程同步了。
三、线程同步
1.锁对象和synchronized
线程同步需要用到锁对象和synchronized的关键字,顾名思义,我们可以通过锁对象给某段代码上锁,保证每次访问这段代码的线程对象只有一个,这样就保证了数据的统一性。synchronized关键字是用于同步的关键字。
2.锁对象的设计
1.首先需要保证锁对象的唯一性
锁唯一可以保证大家都用这一把锁,保证数据的统一性。
2.通常一个类内会设计一个独立的变量作为锁对象的使用3.所有的包装类不可以用于锁对象的使用。
4.当前类的class对象也可以用于锁对象,但是这里不太推荐,因为这个锁的范围太大了,具体原因接下来我会讲到。
3.同步代码块
具体格式
synchronized (锁对象) {
//同步的代码(原子性)
}
当线程执行到同步代码块的时候,此段代码会自动落锁,执行完毕会自动开锁,用此种方法限制了此段的代码的访问对象同一时间有且只有一个。
案例代码
class SaleThread implements Runnable {
//只有一份,用静态来表示票数
private static Integer ticket = 100;
//创建锁对象
private static final Object LOCK = new Object();
@Override
public void run() {
Thread thread = Thread.currentThread();
while (true) {
//同步代码块,锁最小操作
synchronized (LOCK) {
if (ticket > 0) {
System.out.println(thread.getName() + "售出了" + ticket + "号对应的票");
ticket -= 1;
} else {
System.out.println(thread.getName() + "售空");
break;
}
}
}
}
}
/**
* @author: renlon 2023/03/08 10:28
*/
public class Demo1 {
public static void main(String[] args) {
//创建线程对象
Thread t1 = new Thread(new SaleThread(), "A");
Thread t2 = new Thread(new SaleThread(), "B");
Thread t3 = new Thread(new SaleThread(), "C");
//设置优先级
t1.setPriority(Thread.MIN_PRIORITY);
t1.start();
t2.start();
t3.start();
}
}
执行结果展示(为了方便展示,修改票数为10张)
A售出了10号对应的票
C售出了9号对应的票
C售出了8号对应的票
C售出了7号对应的票
C售出了6号对应的票
C售出了5号对应的票
C售出了4号对应的票
C售出了3号对应的票
C售出了2号对应的票
C售出了1号对应的票
C售空
A售空
B售空
4.同步方法
同步方法分为静态修饰的和非静态的,都使用 synchronized 关键字修饰,他们的不同点是约束的范围不同,下面我将通过实例来介绍他们的不同。
1.非静态的同步方法
java代码中有很多类都使用的非静态的同步方法,例如:
ArrayList 和 Vector Vector使用非静态的方法修饰,线程安全
StringBuilder 和 StringBuffer StringBuffer使用非静态的方法修饰,线程安全
HashMap 和 HashTable HashTable使用非静态的方法修饰,线程安全
非静态同步方法的基本格式
public synchronized 返回值 方法名(参数列表) {
同步代码块;
}
代码演示:
class MyThread {
public synchronized void method1() {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("方法1");
}
}
public synchronized void method2() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 10; i++) {
System.out.println("方法2");
}
}
}
/**
* @author: renlon 2023/03/10 23:40
*/
public class Demo2 {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
//同一个实例对象抢占执行一个类的不同非静态同步方法
new Thread(() -> t1.method1()).start();
// new Thread(() -> t1.method1()).start();
new Thread(() -> t2.method2()).start();
}
}
情况一:
当执行t1.method1()和t1.method2()时,线程对象是同一个对象。执行不同的非静态同步方法,method1()中加入了休眠,增加了任务的执行时间长度。可以很清楚地看到,当t1执行MyThread类的其中一个非静态的同步方法时,另一个同步方法则无法执行。
情况二:
当执行t1.method1()和t2.method2()的时候,两个线程不会互相影响,会出现抢占执行的情况
非静态的同步方法是以当前实例化对象为锁对象 ,实例化对象调用任意的 synchronized 修饰的非静态同步方法,则所有其他 synchronized 同步方法无法执行。
2.静态同步方法
基本格式:
public static 返回值 方法名(参数列表) {
//同步操作
}
代码演示:
class A {
public static synchronized void test1() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1");
}
}
public static synchronized void test2() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程2");
}
}
}
/**
* @author: renlon 2023/03/11 16:02
*/
public class Demo3 {
public static void main(String[] args) {
//执行同步任务
new Thread(() -> A.test1()).start();
new Thread(() -> A.test2()).start();
new Thread(() -> A.test1()).start();
}
}
分析:
结果显示三个任务不管怎样都是顺序执行完毕的,任务结果是当一个线程开始执行,其他两个线程任务必须等待当前线程任务执行完毕才可以继续执行。
5.简单总结
static 修饰的 synchronized同步方法,锁对象是当前的 类名.class 。任意线程或对象执行此类的 static 修饰的 synchronized同步方法,则此类其他的这种同步方法都会被锁住,其他任意对象,任意线程都无法执行。
举一个简单上公共厕所的例子:
非静态的同步方法就好比你上公共厕所,锁住了一个坑位,而且同时你这个对象,同一时间内只可以锁一个厕所坑位,
静态的同步方法就是你上一个公共厕所,直接把整个厕所的大门锁上了,任何人在你出来之前都无法再进入这个厕所。