原始代码
package com.dream.test01;
public class MyThread extends Thread{
//共享变量,无论创建多少了对象,这个static是类共享的
private static int ticket =1000;
//构造方法,传入线程名字
public MyThread(String name) {
super(name);
}
@Override
public void run() {
while(ticket>0){
System.out.println(Thread.currentThread().getName()+"正在售出第"+ticket+"张票");
ticket--;
}
}
}
package com.dream.test01;
public class Test01 {
public static void main(String[] args) {
MyThread thread1 = new MyThread("线程1");
MyThread thread2 = new MyThread("线程2");
MyThread thread3 = new MyThread("线程3");
thread1.start();
thread2.start();
thread3.start();
}
}
问题1
因为是多线程问题,因此出现同步问题,当某个线程打印正在卖票的时候,还没来得及将票数减一,另外的线程就开始运行打印了,就会出现打印重复票的问题;
改进1
将打印和票数减一两行代码进行加锁,让某线程在运行这两段代码的时候,其他线程无法进入同步代码块,这里的问题是锁对象应该是谁。
synchronized 锁住某个对象,只要锁住的是同一对象,就能实现同步,但是注意,不是一定要锁住共享变量所代表的那个对象
synchronized实现的机制是锁对象,每个对象只有一把锁,jvm底层是monitorenter和monitorexit,当线程尝试进入synchronized修饰的代码块或者方法时候,会先执行monitorenter指令,首先尝试获取传入的对象的锁(如果是同步方法,对象是this,也就是方法所在的对象),如果这个对象没有被锁定,或者当前这个线程已经获得了这个锁,就会把锁的计数器加一,并且进入代码块内部,代码运行完毕就解锁,转却的说是monitorexit指令执行,并且锁的计数器减一,当锁计数器为0,代表没有线程持有这个对象锁;
因此我们synchronized()括号内的变量只要是传入一个多个线程共享的同一个对象就可以,比如传入的是"abc"都行,因为"abc"在常量池内,是多个线程共享的对象;这里我们选择新建一个Object对象,但是注意放置的位置且必须是static的;
package com.dream.test01;
public class MyThread extends Thread{
private static int ticket =1000;
//这里必须是静态的才行,因为后面创建了三个MyThread对象,但是静态的变量是类共享的
private static Object obj=new Object();
//构造方法,传入线程名字
public MyThread(String name) {
super(name);
}
@Override
public void run() {
while(ticket>0){
synchronized (obj) {
System.out.println(Thread.currentThread().getName()+"正在售出第"+ticket+"张票");
ticket--;
}
}
}
}
package com.dream.test01;
public class Test01 {
public static void main(String[] args) {
MyThread thread1 = new MyThread("线程1");
MyThread thread2 = new MyThread("线程2");
MyThread thread3 = new MyThread("线程3");
thread1.start();
thread2.start();
thread3.start();
}
}
问题2
同步的问题已经解决了,但是出现了-1
的情况,原因如下是当ticket1时,线程1进入while,整准备进入synchronized,时间片到了,这时线程2一看,ticket1,也进入while,然后线程1继续进入synchronized执行,执行后ticket为0,然后线程2继续进入synchronized执行,执行后ticket=-1了。
改进2
在进入synchronized后再次判断ticket的值即可
package com.dream.test01;
public class MyThread extends Thread{
private static int ticket =1000;
//这里必须是静态的才行,因为后面创建了三个MyThread对象,但是静态的变量是类共享的
private static Object obj=new Object();
//构造方法,传入线程名字
public MyThread(String name) {
super(name);
}
@Override
public void run() {
while(ticket>0){
synchronized (obj) {
if (ticket>0) {
System.out.println(Thread.currentThread().getName()+"正在售出第"+ticket+"张票");
ticket--;
if (ticket==0) {
System.out.println(Thread.currentThread().getName()+"票已售完");
}
}else {
System.out.println(Thread.currentThread().getName()+"票已售完");
}
}
}
}
}
扩展
问题已经解决了,但是我们使用的是同步代码块的方法,还有同步方法和lock的两种方法
同步方法
同步方法,注意同步方法的锁对象是this,就是这个方法所在的类的对象,因为后面新建了三个MyThread 这个类的对象,因此为了保证同一个锁对象,对这个方法使用static即可,这样就是属于类的
package com.dream.test02;
public class MyThread extends Thread{
private static int ticket =1000;
public MyThread(String name) {
super(name);
}
@Override
public void run() {
while(ticket>0){
sell();
}
}
//同步方法,注意同步方法的锁对象是this,就是这个方法所在的类的对象,
//因为后面新建了三个MyThread 这个类的对象,因此为了保证同一个所对象,
//对这个方法使用static即可,这样就是属于类的
private static synchronized void sell() {
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"正在售出第"+ticket+"张票");
ticket--;
if(ticket == 0){
System.out.println(Thread.currentThread().getName() + "票已经销售完毕");
}
}else {
System.out.println(Thread.currentThread().getName()+"票已售完");
// Thread.currentThread().interrupt();
}
}
}
package com.dream.test02;
public class Test01 {
public static void main(String[] args) {
MyThread thread1 = new MyThread("线程1");
MyThread thread2 = new MyThread("线程2");
MyThread thread3 = new MyThread("线程3");
thread1.start();
thread2.start();
thread3.start();
}
}
Lock
Lock也要求是同一个锁对象,因此在创建的时候,也用static修饰,让他属于这个类
package com.dream.test03;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyThread extends Thread{
private static int ticket =1000;
//因为后面要创建多个MyThread,为了保证同一个锁对象,因此用static修饰
private static Lock lock = new ReentrantLock();
public MyThread(String name) {
super(name);
}
@Override
public void run() {
while(ticket>0){
lock.lock();
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"正在售出第"+ticket+"张票");
ticket--;
if(ticket == 0){
System.out.println(Thread.currentThread().getName() + "票已经销售完毕");
}
}else {
System.out.println(Thread.currentThread().getName()+"票已售完");
break;
// Thread.currentThread().interrupt();
}
lock.unlock();
}
}
}
package com.dream.test03;
public class Test01 {
public static void main(String[] args) {
MyThread thread1 = new MyThread("线程1");
MyThread thread2 = new MyThread("线程2");
MyThread thread3 = new MyThread("线程3");
thread1.start();
thread2.start();
thread3.start();
}
}
总结
以上问题的精华是什么?答案是同一个锁对象,那锁对象的精华是什么,就是一个对象而已,千万不要理解为锁对象是要可能会出现同步问题的那段代码。
举个例子,一群猴子站在一堆香蕉旁边,要求一个一一个的去拿香蕉,不能哄抢,用多线程的思想就是,规定一个锁对象,只有拿到这个锁对象的猴子才能去拿香蕉,拿完香蕉,释放这个锁对象,然后其他猴子再去拿到锁对象,才能去拿香蕉,那么这个锁对象是什么呢,我可以规定是猴子家园里面的某一个独一无二棍子,也可以规定是某一个独一无二的石头,反正是独一无二的,且是属于这个猴子家园的,每个猴子都可以得到,但是同一时刻,只能有一个猴子可以拿着的东西就行了,不能怎么样呢?不能是这个猴子拿着棍子去,那个猴子拿着石头去,这就不是同一个锁对象了嘛。
总之,你要让同步代码块,同步方法,或者是lock,底层的锁对象是一个多个线程共享的独一无二的对象即可。