上篇文章中介绍了什么是线程安全,也就是主要给线程安全下了一个定义,本篇文章将主要介绍实现线程安全中的一种方式:使用 java 原生的 Synchronized 关键字。
Synchronized
使用Synchronized实现线程安全
相信大多数小伙伴对这个关键字并不陌生。使用该关键字可以保证在同一把锁的前提下,在它所包含的代码块及方法中只能有一个线程访问,继续沿用上篇文章中的例子。
public class TestCase {
static class Task1 implements Runnable{
private int i = 0;
@Override
public void run() {
for (int j = 0; j < 1000; j++) {
i++;
}
}
public int getValue(){
return i;
}
}
public static void main(String[] args) throws InterruptedException {
Task1 task1 = new Task1();
for (int i = 0; i < 10; i++) {
new Thread(task1).start();
}
Thread.sleep(3000);
System.out.println(task1.getValue());
}
其实保证firstCase线程安全的方法很简单,我们只需要使用Synchronized关键字就可以了,那么下面给出实现后的代码。
public void run() {
synchronized(this){
for (int j = 0; j < 1000; j++) {
i++;
}
}
}
Synchronized代码块与Synchronized方法
既然 Synchronized 代码块与 Synchronized 方法功能如此相似那么他们的实现原理是不是一样的呢?话不多说,我们使用javap -verbose 读一下class文件看一下它们的实现方式。
public class TestCase2 {
public static synchronized void method(){
System.out.println("hello");
}
public static void method2(){
synchronized(TestCase.class){
System.out.println("hello");
}
}
public static void main(String[] args) {
method();
method2();
}
}
通过 class 文件我们大概能了解到了这两种实现的区别:
Synchronized 修饰的方法与其他修饰符一样,是通过 ACC_SYNCHRONIZED 关键字来修饰该方法。
Synchronized 修饰的代码块在底层是通过 monitorenter 与 monitorexit 来保证,它就类似于一个监视器,保证在此区间内只能由一个线程来访问这段代码。可能有细心的小伙伴发现这样一件事,为什么 monitorenter 之后要进行两次 monitorexit 呢?其实原因很简单,了解过 System.out.println() 源码的小伙伴可能会更容易理解一些
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
public void print(String s) {
if (s == null) {
s = "null";
}
write(s);
}
private void write(String s) {
try {
synchronized (this) {
ensureOpen();
textOut.write(s);
textOut.flushBuffer();
charOut.flushBuffer();
if (autoFlush && (s.indexOf('\n') >= 0))
out.flush();
}
}
catch (InterruptedIOException x) {
Thread.currentThread().interrupt();
}
catch (IOException x) {
trouble = true;
}
}
通过了解源码我们可以看出,在println 代码的实现中可能会抛出异常非正常结束代码,那么面对这样的情况我们必须让程序非正常结束的时候释放锁以便之后其他线程不会因为所没有释放而一直处于阻塞状态。
可能又有的小伙伴有疑问了,为什么多次进入Synchronized 代码块并没有多个monitorenter 呢?这是因为没进入一次Synchronized 监视器计数器会进行加一操作,释放一次锁会进行减一操作。
Synchronized 与对象的可见性
介绍可见性之前我们需要一点额外的知识,那就是jmm内存模型。
jmm中明确规定java线程不能直接与主内存直接进行交互,都要通过工作进内存间接与主内存进行交互,而每个线程的工作内存间相互独立。工作内存中一般都是存的需要操作的变量的副本,经过cpu进行处理后,再将工作内存中处理后的变量值同步回主内存,其实现细节会在之后的文章里提到。
理解了内存模型之后的可见性就很好解释了,线程1与线程2同时读取一个变量到工作内存,线程1操作完之后将数据写回到主内存,但是线程2并不知道,可能还是用的是之前的过期数据并没有及时的从主内存中读取。这种情况是非常危险的:如果线程1通过一个变量flag来决定while循环暂停的时机,当线程1将flag = false 写回自主内存而线程2并没有更新一直使用工作内存中过期的数据就会造成很严重的后果。
Synchronized 中的另外一个作用就是保证代码经过同步代码块之后该变量的状态对其他线程是立即可见的。
有不足的地方欢迎指正,如需转载请注明出处,谢谢