需求:多个线程同时卖票
class MyThread implements Runnable {
private int ticket = 10;
@Override
public void run() {
while (this.ticket > 0)
{
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"还剩下"+this.ticket--+"票");
}
}
}
public class TestThread {
public static void main(String[] args) {
MyThread myThread = new MyThread();
new Thread(myThread,"黄牛1").start();
new Thread(myThread,"黄牛2").start();
new Thread(myThread,"黄牛3").start();
}
}
Files\Java\jdk1.8.0_181\jre\lib\rt.jar;E:\Java\code\out\production\code" www.bit.java.TestThread
黄牛3还剩下10票
黄牛1还剩下9票
黄牛2还剩下10票
黄牛3还剩下8票
黄牛1还剩下6票
黄牛2还剩下7票
黄牛3还剩下5票
黄牛2还剩下3票
黄牛1还剩下4票
黄牛2还剩下2票
黄牛3还剩下1票
黄牛1还剩下0票
黄牛2还剩下-1票
Process finished with exit code 0
这个时候我们发现,票数竟然出现负数,这种问题我们称之为不同步操作。
不同步的唯一好处是处理速度快(多个线程并发执行)
所谓的同步指的是所有的线程不是一起进入到方法中执行,而是按照顺序一个一个进来。
如果要想实现这把"锁"的功能,可以采用关键字synchronized来处理。
使用synchronized关键字处理有两种模式:同步代码块、同步方法
synchronized(对象)
,一般可以锁定当前对象this。表示同一时刻只有一个线程能够进入同步代码块,但是多个线程可以同时进入方法。class MyThread implements Runnable {
private int ticket = 1000;
@Override
public void run() {
for(int i = 0;i< 1000;i++){
synchronized (this){
if(this.ticket > 0)
{
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"还剩下"+this.ticket--+"票");
}
}
}
}
}
public class TestThread {
public static void main(String[] args) {
MyThread myThread = new MyThread();
new Thread(myThread,"黄牛1").start();
new Thread(myThread,"黄牛2").start();
new Thread(myThread,"黄牛3").start();
}
}
方法上加synchronized
,表示此时只有一个线程能够进入同步方法。class MyThread implements Runnable {
private int ticket = 1000;
@Override
public void run() {
for(int i = 0;i< 1000;i++){
SellTicket(this.ticket);
}
}
private synchronized void SellTicket(int ticket){
if(this.ticket > 0)
{
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"还剩下"+this.ticket--+"票");
}
}
}
public class TestThread {
public static void main(String[] args) {
MyThread myThread = new MyThread();
new Thread(myThread,"黄牛1").start();
new Thread(myThread,"黄牛2").start();
new Thread(myThread,"黄牛3").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 TestThread {
public static void main(String[] args) {
for(int i = 0;i < 3;i++){
new Thread(new MyThread()).start();
}
}
}
通过上述代码以及运行结果我们可以发现,没有看到synchronized起到作用,三个线程同时运行test()方法。
实际上,synchronized(this)
以及非static的synchronized方法
,只能防止多个线程同时执行同一个对象的同步代码段。即synchronized锁住的是括号里的对象,而不是代码。对于非static的synchronized方法,锁的就是对象本身也就是this。
当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{
private Sync sync;
public MyThread(Sync sync){
this.sync = sync;
}
@Override
public void run() {
this.sync.test();
}
}
public class TestThread {
public static void main(String[] args) {
Sync sync = new Sync();
for(int i = 0;i < 3;i++){
new Thread(new MyThread(sync)).start();
}
}
}
class Sync {
public synchronized 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 TestThread {
public static void main(String[] args) {
for(int i = 0;i < 3;i++){
new Thread(new MyThread()).start();
}
}
}
上面代码用synchronized(Sync.class)
实现了全局锁的效果。因此,如果要想锁的是代码段,锁住多个对象的同一方法,使用这种全局锁,锁的是类而不是this。
static synchronized方法
,static方法可以直接类名加方法名调用,方法中无法使用this,所以它锁的不是this,而是类的Class对象,所以,static synchronized方法也相当于全局锁,相当于锁住了代码段。
class Sync {
public static 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 TestThread {
public static void main(String[] args) {
for(int i = 0;i < 3;i++){
new Thread(new MyThread()).start();
}
}
}
先来看一段简单的代码:
public class Test{
private static Object object = new Object();
public static void main(String[] args) {
synchronized (object) {
System.out.println("hello world");
}
}
}
下面我们使用javap反编译后看看生成的部分字节码
...
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #2 // Field object:Ljava/lang/Object;
3: dup
4: astore_1
5: monitorenter // 瞪大眼睛看这里!!!
6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
9: ldc #4 // String hello world
11: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
14: aload_1
15: monitorexit // 瞪大眼睛看这里!!!
16: goto 24
19: astore_2
20: aload_1
21: monitorexit // 瞪大眼睛看这里!!!
22: aload_2
23: athrow
24: return
Exception table:
from to target type
6 16 19 any
19 22 19 any
...
执行同步代码块后首先要先执行monitorenter
指令,退出的时候monitorexit
指令。通过分析之后可以看出,使用 Synchronized进行同步,其关键就是必须要对对象的监视器monitor进行获取,当线程获取monitor后才能继续往下执行,否则就只能等待。而这个获取的过程是互斥的,即同一时刻只有一个线程可以获取到该对象的monitor监视器。
上述字节码中包含一个monitorenter
指令以及多个monitorexit
指令。这是因为Java虚拟机需要确保所获得的锁在正常执行路径,以及异常执行路径上都能够被解锁。
public synchronized void test();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=0, args_size=0
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String hello world
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 471: 0
line 472: 8
当使用synchronized标记方法时,字节码会出现一个访问标记ACC_SYNCHRONIZED
。该标记表示在进入方法时,JVM需要进行monitorenter
操作。在退出方法时,无论是正常返回,还是向调用者抛异常,JVM需要进行monitorexit
操作。
这里 monitorenter
和 monitorexit
操作所对应的锁对象是隐式的。对于实例方法来说,这两个操作对应的锁对象是 this;对于静态方法来说,这两个操作对应的锁对象则是所在类的 Class 实例。
当JVM执行monitorenter时,如果目标对象monitor的计数器为0,表示此时该对象没有被其他线程所持有。此时JVM会将该锁对象的持有线程设置为当前线程,并且将monitor计数器+1。
在目标锁对象的计数器不为0的情况下,如果锁对象的持有线程是当前线程,JVM可以将计数器再次+1(可重入锁);否则需要等待,直到持有线程是释放线程。
当执行monitorexit时,JVM需将锁对象计数器-1。当计数器减为0时,代表该锁以及被释放掉,唤醒所有正在等待的线程去竞争该锁。
之所以采用这种计数器的方式,是为了允许同一个线程重复获取同一把锁。举个例子,如果一个 Java 类中拥有多个 synchronized 方法,那么这些方法之间的相互调用,不管是直接的还是间接的,都会涉及对同一把锁的重复加锁操作。因此,我们需要设计这么一个可重入的特性,来避免编程里的隐式约束。
class MyThread extends Thread{
public synchronized void A(){
while(true){}
}
public synchronized void B(){
System.out.println(Thread.currentThread().getName()+",线程B...");
}
@Override
public void run() {
A();
B();
}
}
public class TestThread {
public static void main(String[] args) {
MyThread myThread = new MyThread();
new Thread(myThread,"A").start();
new Thread(myThread,"B").start();
}
}
class MyThread extends Thread{
public synchronized void A(){
while(true){
System.out.println(Thread.currentThread().getName()+",线程A...");
B();
}
}
public synchronized void B(){
System.out.println(Thread.currentThread().getName()+",线程B...");
}
@Override
public void run() {
A();
B();
}
}
public class TestThread {
public static void main(String[] args) {
MyThread myThread = new MyThread();
new Thread(myThread,"A").start();
new Thread(myThread,"B").start();
}
}