Java中可以使用内置锁机制来支持原子性。具体来说可以使用Synchronized 来修饰代码块,被修饰的代码块在同一个时间只能由获取了锁的线程执行。
同步代码块的锁就是方法调用所在的对象。静态的synchronized方法以Class对象当锁。线程会在执行代码块之前尝试获取锁,如果获取不到就进行等待,无论以何种方式执行结束都会释放锁。
同步代码块使用对象作为锁
/**
* @author eventime
* 同步代码块演示代码
*/
public class SynchronizedTest {
private int getValue() {
// 定义第一个getValue操作
return 1;
}
private int getValue2() {
return 2;
}
public static void main(String[] args) {
SynchronizedTest test = new SynchronizedTest();
Thread t1 = new Thread(()-> System.out.println(test.getValue()));
Thread t2 = new Thread(()-> System.out.println(test.getValue2()));
t1.start();
t2.start();
}
}
执行上面无同步代码块的程序,得到的结果总是1 2 或者 2 1。
这是因为代码运行时共有3个thread(main, t1, t2),他们之间并不是串行执行,而是并发执行。
/**
* @author eventime
* 同步代码块演示代码
*/
public class SynchronizedTest {
private synchronized int getValue() {
// 定义第一个getValue操作
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return 1;
}
private synchronized int getValue2() {
// 定义第二个getValue操作
return 2;
}
public static void main(String[] args) {
SynchronizedTest test = new SynchronizedTest();
Thread t1 = new Thread(()-> System.out.println(test.getValue()));
Thread t2 = new Thread(()-> System.out.println(test.getValue2()));
t1.start();
// 主线程sleep,留时间给t1获取锁。
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
t2.start();
}
}
我们修改了代码,将两个函数使用synchronized修饰。
在调用t1.start()与t2.start() 之间加入了sleep留给t1充足的时间去获取对象的锁。程序执行结果为 1 2。
分析执行结果我们可以看到,在t1获取了test对象的锁后,会先进行5s的休眠。而这个时候主线程1s的休眠结束执行t2.start();而因为test的锁已经被t1获取,那么t2只能等t1执行完毕释放锁后再获取锁执行。
值得注意的是,如果代码块没有被 synchronized 修饰那么访问这个代码块便不需要获取锁,在本例中,如果将getValue2的synchronized去除,那么即使锁被t1占用,t2依旧能正常执行,在主线程睡眠结束后便可输入。最后的执行结果为: 2 1。
可重入机制
简单来说,可重入机制是为了使得当前拥有锁的进程可以再次进入同步代码块。
/**
* @author eventime
* 同步代码块演示代码
*/
public class SynchronizedTest {
private synchronized int getValue() {
// 定义第一个getValue操作
//这里会首先调用getValue2()
System.out.println(getValue2());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return 1;
}
private synchronized int getValue2() {
// 定义第二个getValue操作
return 2;
}
public static void main(String[] args) {
SynchronizedTest test = new SynchronizedTest();
Thread t1 = new Thread(()-> System.out.println(test.getValue()));
Thread t2 = new Thread(()-> System.out.println(test.getValue2()));
t1.start();
// 主线程sleep,留时间给t1获取锁。
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
t2.start();
}
}
首先java的同步代码块已经实现了可重入的机制,我们只需要理解他的使用场景。
在上例中,t1已经获取了对象test的锁,我们也知道当线程需要执行同步代码块的时候需要获取锁。在更改后的getValue()方法中,会去执行同步代码块getValue2()。
如果没有可重入机制,那么线程t1会再次去尝试获取test对象的锁,然而这个锁已经被自身获取。无法获取锁那么将无法执行下去,无法执行完毕便无法释放锁。这就出现了死锁。
在可重入机制下,在判断线程t1已经获取了test的锁后,便可直接去执行getValue2()或者其他同步代码块,从而避免了上述问题。具体来说每个锁都会有一个计数值和一个所有者线程,当所有者线程请求锁时,便会在计数值上增加1,当同步代码块执行完毕后,便会在计数值减去1。当技术值为0时便会释放锁。