前言
在上一篇 多线程(一)、基础概念及notify()和wait()的使用 文章中我们讲了多线程的一些基础概念还有等待通知机制,在讲线程之间共享资源的时候,提到会出现数据不同步问题,我们先通过一个示例来演示这个问题。
/**
* @author : EvanZch
* description:
**/
public class SynchronizedTest {
// 赋count初始值为0
public static int count = 0;
// 进行累加操作
public void add() {
count++;
}
public static class TestThread extends Thread {
private SynchronizedTest synchronizedTest;
public TestThread(SynchronizedTest synchronizedTest) {
this.synchronizedTest = synchronizedTest;
}
@Override
public void run() {
super.run();
// 执行10000次累加
for (int x = 0; x < 10000; x++) {
synchronizedTest.add();
}
}
}
public int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
SynchronizedTest synchronizedTest = new SynchronizedTest();
// 开启两个线程
new TestThread(synchronizedTest).start();
new TestThread(synchronizedTest).start();
int count = synchronizedTest.getCount();
System.out.println("count=" + count);
}
}
可以看到,我程序中我们启动了两个线程,同时对 Count
变量进行累加操作,每个线程循环累加10000次,我们预想的结果,获取的count值应该会是20000,执行程序可以发现。
0?为什么结果会是0?因为我们在main里面开启线程执行,方法是顺序执行,当执行到 输出语句的时候,线程run方法还没有启动,所以这里打印的是count的初始值 0;
怎么获取到正确结果?
1、等待一会在获取结果
public static void main(String[] args) throws InterruptedException {
SynchronizedTest synchronizedTest = new SynchronizedTest();
new TestThread(synchronizedTest).start();
new TestThread(synchronizedTest).start();
// 等待一秒再回去结果
Thread.sleep(1000);
int count = synchronizedTest.getCount();
System.out.println("count=" + count);
}
我们在获取结果之前,先等待一秒,结果如下:
结果不再为 0 ,但是结果也不是我们预想的 20000啊,难道是等待时间不够?我们增加等待时间,在运行,发现结果也不是20000,这么看,使用等待时间不严谨,因为没办法判断线程执行结束时间(其实线程执行很快的,远不需要几秒),那我们可以使用 join方法。
2、thread.join()
我们先看一下 thread 的 join方法
/**
* Waits for this thread to die.
*
* <p> An invocation of this method behaves in exactly the same
* way as the invocation
*
* <blockquote>
* {@linkplain #join(long) join}{@code (0)}
* </blockquote>
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public final void join() throws InterruptedException {
join(0);
}
注释大概意思是:当调用join方法后,会进行阻塞,直到该线程任务执行结束。
可以让线程顺序执行。
那我们可以简单修改代码,让两个线程执行结束后再打印结果
这里需要注意,我们是在 main 这个线程里面调用 join 方法, 则两个线程会在main 线程阻塞,但是两个子线程还是在并行处理,都执行结束后才会唤醒 main 线程执行后续操作。
public static void main(String[] args) throws InterruptedException {
SynchronizedTest synchronizedTest = new SynchronizedTest();
TestThread testThread = new TestThread(synchronizedTest);
TestThread testThread1 = new TestThread(synchronizedTest);
testThread.start();
testThread1.start();
// 让程序顺序执行
testThread.join();
testThread1.join();
// 当两个线程任务结束后再获取结果
int count = synchronizedTest.getCount();
System.out.println("count=" + count);
}
结果:
发现结果也不是我们预想的 20000,我们使用了 join()
方法,它会在调用线程进行阻塞(main),当testThread
和 testThread1
都执行结束后再唤醒调用线程 , 能确保两个线程肯定是执行结束了的,可是结果跟预期不一致,多次打印,发现结果一直在 10000 ~ 20000 这个区间波动。
为什么会出现这种情况?
上一篇文章讲过,同一个进程的多个线程共享该进程的所有资源,当多个线程同时访问一个对象或者一个对象的成员变量,可能会导致数据不同步问题,比如 线程A 对数据a进行操作,需要从内存中进行读取然后进行相应的操作,操作完成后再写入内存中,但是如果数据还没有写入内存中的时候,线程B 也来对这个数据进行操作,取到的就是还未写入内存的数据,导致前后数据同步问题,我们也叫线程不安全操作。
比如 线程 A 取到 count 的时候,其值为 100,加 1 后再放入内存中,如果在放入内存之前 线程B 也来拿 count 并对其进行累加操作,这个时候 **线程B **取到的 count 值 还是100,加 1 后放入内存,这个时候值为101, 这样 线程 A 进行累加的那步操作就没有被算上,这就是为啥,最后两个线程算出来的结果肯定是小于 20000。
怎么避免这种情况?
我们知道出现这种情况的原因是操作的时候,因为多个线程同时访问一个对象或者对象的成员变量,要处理这个问题,我们就引入了关键字 synchronized
正文
一、内置锁 synchronized
关键字 synchronized
可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。
锁又分为对象锁和类锁:
对象锁: 对一个对象实例进行锁操作,实例对象可以有多个,不同对象实例的对象锁互不干扰。
类锁:用于类的静态方法或者类的Class对象进行锁操作。我们知道每个类只有一个Class对象,也就只有一个类锁。
注意点:
类锁只是一个概念上的东西,它锁的也是对象,只不过这个对象是类的Class对象,其唯一存在。
类锁和对象锁之间互不干扰。
通过上面的案例,我们简单改改,我们在执行累加方法上加上 synchronized
关键字,然后再运行。
/**
* @author : EvanZch
* description:
**/
public class SynchronizedTest {
public static int count = 0;
// 我们对add方法添加关键字 synchronized
public synchronized void add() {
count++;
}
public static class TestThread extends Thread {
private SynchronizedTest synchronizedTest;
public TestThread(SynchronizedTest synchronizedTest) {
this.synchronizedTest = synchronizedTest;
}
@Override
public void run() {
super.run();
for (int x = 0; x < 10000; x++) {
synchronizedTest.add();
}
}
}
public int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
SynchronizedTest synchronizedTest = new SynchronizedTest();
TestThread testThread = new TestThread(synchronizedTest);
TestThread testThread1 = new TestThread(synchronizedTest);
testThread.start();
testThread1.start();
// 让程序顺序执行
testThread.join();
testThread1.join();
int count = synchronizedTest.getCount();
System.out.println("count=" + count);
}
}
结果:
可以看到我们只加了一个关键字 synchronized
,结果就跟我们预期的 20000 一致,我们将 synchronized
添加到方法上,就确保了多个线程同一时刻只有一个线程对此方法进行操作,这样就确保了线程安全问题。
前面说了内置锁存在对象锁和类锁 ,我们来看一下具体怎么实现和区别。
1.1、对象锁
对一个对象实例进行锁操作,实例对象可以有多个,不同对象实例的对象锁互不干扰。
我们在前面的示例上进行更改。
方法锁:
// 非静态方法
public synchronized void add() {
count++;
}
同步代码块锁:
public void add(){
synchronized (this){
count ++;
}
}
或者:
// 非静态变量
public Object object = new Object();
public void add(){
synchronized (object){
count ++;
}
}
我们可以看到对象锁都是对非静态方法和非静态变量进行加锁,以上三种从本质上来说没有区别,我们这个时候再改一下我们的示例代码,来验证一下 不同对象实例的对象锁互不干扰。
public static void main(String[] args) throws InterruptedException {
SynchronizedTest synchronizedTest = new SynchronizedTest();
// 我们再创建一个 SynchronizedTest 对象
SynchronizedTest synchronizedTest1 = new SynchronizedTest();
// 传入 synchronizedTest
TestThread testThread = new TestThread(synchronizedTest);
// 传入 synchronizedTest1
TestThread testThread1 = new TestThread(synchronizedTest1);
testThread.start();
testThread1.start();
// 让程序顺序执行
testThread.join();
testThread1.join();
int count = synchronizedTest.getCount();
System.out.println("count=" + count);
}
我们开启两个线程,分别传入了不同的实例对象,这个时候再多次运行,查看运行结果。
结果:
我们多次运行获取结果,发现都获取不到我们期望的20000,可以我们明明也在add()
方法上添加了 synchronized
啊,唯一不同的就是,两个线程传入了不同的对象,所以通过结果,我们可以得出,不同对象的对象锁之间,是互不影响,各种运行。
1.2、类锁
用于类的静态方法或者类的Class对象进行锁操作。我们知道每个类只有一个Class对象,也就只有一个类锁。
类锁其实也是对象锁,只不过锁的对象比较特殊。
静态方法锁:
// 静态方法
public static synchronized void add() {
count++;
}
同步代码块锁:
public void add(){
// 传入Class对象
synchronized (SynchronizedTest.class){
count ++;
}
}
或者:
// 静态成员变量
public static Object object = new Object();
public void add(){
synchronized (object){
count ++;
}
}
我们知道静态变量和类的Class对象在内存中只存在一个,所以我们对add
方法通过类锁方式进行加锁,不管外界这个时候传的对象有多少个,它也是唯一的,我们再执行上面的main方法,打印结果:
可以看到结果和期望一致。
知识拓展 :static 关键字和 new 一个对象,做了什么操作?
static 关键字:
- 静态变量是随着类加载时被完成初始化的,它在内存中仅有一个,且 JVM 也只会为它分配一次内存,同时类所有的实例都共享静态变量,即一处变、处处变,可以直接通过类名来访问它。
- 但是实例变量则不同,它是伴随着new实例化的,每创建一个实例就会产生一个实例变量,它与该实例同生共死。
new 一个对象,底层做了啥?
1、Jvm加载未加载的字节码,开辟空间
2、静态初始化(1静态代码块和2静态变量)
3、成员变量初始化(1普通代码块和2普通成员变量)
4、构造器初始化(构造函数)