同步问题:每一个线程对象轮番抢占共享资源带来的问题.
class myThread implements Runnable{
private int t = 100;
@Override
public void run() {
while (t > 0) {
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "剩余" + --t + "张票");
}
}
}
public class Test {
public static void main(String[] args) {
myThread mt = new myThread();
new Thread(mt,"窗口A").start();
new Thread(mt,"窗口B").start();
new Thread(mt,"窗口C").start();
}
}
这个代码实现了三个窗口同时卖票,这里的票就是一个共享资源,但运行时会发现有时候票数会为-1,这显然是不可能的,所以这是一个不同步操作。(不同步的唯一好处是处理速度快,因为它是多个线程并发执行)。
一、同步处理
所谓的同步指的是所有的线程不是一起进入到方法中执行,而是按照顺序一个一个进来。
1.使用内置关键字synchronized关键字来处理同步问题
(内建锁)synchronized关键字处理同步有2种模式:同步代码块、同步方法
同步代码块:要使用同步代码块,必须要设置一个锁定的对象,一般可以锁当前对象this
class myThread implements Runnable{
private int t = 100;
@Override
public void run() {
synchronized (this) {
while (t > 0) {
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "剩余" + --t + "张票");
}
}
}
}
public class Test {
public static void main(String[] args) {
myThread mt = new myThread();
new Thread(mt,"窗口A").start();
new Thread(mt,"窗口B").start();
new Thread(mt,"窗口C").start();
}
}
同步方法:在方法上添加一个关键字synchronized,表示此方法只有一个线程能进入。隐式锁对象this
class myThread implements Runnable{
int t = 100;
@Override
public void run() {
for (int i = 0;i<100;i++){
sale();
}
}
public synchronized void sale(){
if(t > 0){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "剩余" + t-- + "张票");
}
}
}
class Test{
public static void main(String[] args) {
myThread mt = new myThread();
Thread thread = new Thread(mt,"窗口A");
Thread thread2 = new Thread(mt,"窗口B");
Thread thread3 = new Thread(mt,"窗口C");
thread.start();
thread2.start();
thread3.start();
}
}
synchronized的额外说明
先看这样一段代码
class Sync {
public synchronized void test() {
System.out.println("test方法开始,当前线程为 "+Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test方法结束,当前线程为 "+Thread.currentThread().getName());
}
}
class MyThread extends Thread {
@Override
public void run() {
Sync sync = new Sync() ;
sync.test();
}
}
public class Test {
public static void main(String[] args) {
for (int i = 0; i < 3 ; i++) {
Thread thread = new MyThread() ;
thread.start();
}
}
}
输出
可以发现并没有锁住,三个线程同时运行test方法,原因在于synchronized(this)以及非static的synchronized方法,只能防止多个线程同时执行同一个对象的同步代码段。这里调用test方法的是sync对象,而sync对象是产生了三个,所以三个sync对象可以同时运行test方法。 要想解决这个问题,最容易想到的就是只让Sync产生一个对象
class Sync {
public synchronized void test() {
System.out.println("test方法开始,当前线程为 " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test方法结束,当前线程为 " + Thread.currentThread().getName());
}
}
class MyThread extends Thread {
Sync sync;
public MyThread(Sync sync){
this.sync = sync;
}
@Override
public void run() {
sync.test();
}
}
public class Test {
public static void main(String[] args) {
Sync sync = new Sync() ;
for (int i = 0; i < 3 ; i++) {
Thread thread = new MyThread(sync) ;
thread.start();
}
}
}
输出
还有一种方法就是我们可以锁住这个类对应的Class对象,也同样可以实现上面的效果。
class Sync {
public void test() {
synchronized (Sync.class) {
System.out.println("test方法开始,当前线程为 " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test方法结束,当前线程为 " + Thread.currentThread().getName());
}
}
}
class MyThread extends Thread {
@Override
public void run() {
Sync sync = new Sync() ;
sync.test();
}
}
public class Test {
public static void main(String[] args) {
for (int i = 0; i < 3 ; i++) {
Thread thread = new MyThread() ;
thread.start();
}
}
}
上面代码用synchronized(Sync.class)实现了全局锁的效果。因此,如果要想锁的是代码段,锁住多个对象的同一方法,使用这种全局锁,锁的是类而不是this。
static synchronized方法,static方法可以直接类名加方法名调用,方法中无法使用this,所以它锁的不是this,而是类的Class对象,所以,static synchronized方法也相当于全局锁,相当于锁住了代码段。
2.synchronized的使用场景
3.synchronized底层实现
对象锁(monitor)机制 ——JDK6之前的synchronized(重量级锁)
看一段简单的代码,使用同步代码块
public class Test{
private static Object object = new Object();
public static void main(String[] args) {
synchronized (object) {
System.out.println("hello world");
}
}
}
进行反编译
主要看里面的monitorenter和monitorexit。
在同步代码块中,执行同步代码块后,首先要执行monitorenter指令,退出时执行monitorexit指令
使用内建锁(synchronized)进行同步,关键在于要获取指定锁对象monitor对象,当线程获取monitor后才能继续向下执行,否在就只能等待。这个获取过程是互斥的,即同一时刻只有一个线程能够获取到对象monitor。
通常一个monitorenter指令会包含若干个monitorexit指令,。原因在于JVM需要确保锁在正常执行路径以及异常执行路径上都能够正确解锁
使用同步方法:
当使用synchronized标记方法时,编译后的字节码中方法的访问标记多了一个ACC_SYNCHRONIZED。该标记表示,进入该方法时,JVM需要进行monitorenter操作,退出该方法时,无论是否正常返回,JVM均需要进程monitorexit操作
关于 monitorenter 和 monitorexit 的作用,我们可以抽象地理解为每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。当执行monitorenter时,如果目标锁对象的monitor计数器为0,表示此对象没有被其他任何对象所持有,此时JVM会将该锁对象的持有线程设置为当前线程,并且将计数器+1;如果目标锁对象的计数器不为0,判断锁对象的持有线程是否是当前线程,如果是再次将计数器+1(锁的可重入性),如果锁的持有线程不是当前线程,当前线程需要等待,直到持有线程释放锁。当执行monitorexit时,JVM会将锁对象的计数器-1,当计数器值减为0时,代表该锁对象已经被释放。