线程同步的引入
问:为何要使用同步?
答:Java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查), 将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。
案例:电影院售票
需求:某电影院目前正在上映贺岁大片,共有100张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票。
通过售票案例我们看到结果出现负票的情况
因为三个线程同时执行,互相抢CPU的执行权。在进行判断时,三个线程都进入了判断语句,同时去对票数进行减减操作,这样就会出现减减多次变成负数的情况,那么应该怎么解决?
答:我们应该学习开关门原则,一旦有线程判断完,进去执行卖票,其他线程无法再进行判断进去卖票,把卖票的代码锁起来。同一时刻只能有一个线程进去执行,等这个线程执行完了,其他线程才能进去判断执行。
同步方式
1.同步代码块
即有synchronized关键字修饰的语句块。
被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。
代码如:
synchronized(object){
//加锁(同步)部分
//同一个时刻只能有一个线程进来执行
}
锁对象问题(object):只要保证唯一就可以
常见的锁对象:
自己搞一个类(接口)全是静态常量(必须是对象)。
类对象。
当前类的静态属性。
等等…
注:同步是一种高开销的操作,因此应该尽量减少同步的内容。
通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
注:在Thread的子类中,this无法充当锁对象。
在Runnable接口的实现类中,同步代码块的锁对象可以使用this,只要保证该实现类new一次取使用即可。
电影院售票代码实例:
1.继承Thread实现例子代码:
1.SellTicketThread.java:
public class SellTicketThread extends Thread {
// static修饰变量:所有对象调用这个共同的属性,一改全改
static int ticket = 100;
public SellTicketThread(String name) {
super(name);
}
@Override
public void run() {
System.out.println(getName() + "-->开始售票了!");
while (true) {
//同步代码块要放在循环内部
synchronized (Math.class) {
if (ticket > 0) {
try {
sleep(200);
System.out.println(getName() + "卖出第 " + (ticket--) + " 张票");
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
System.out.println(getName() + "停止售票!!!!!!");
}
}
2.Test.java:
public class Test {
public static void main(String[] args) {
SellTicketThread mt1 = new SellTicketThread("窗口1");
SellTicketThread mt2 = new SellTicketThread("窗口2");
SellTicketThread mt3 = new SellTicketThread("窗口3");
mt1.start();
mt2.start();
mt3.start();
}
}
2.实现Runable接口例子代码:
1.SellTicketThread.java:
public class SellTicketThread implements Runnable {
int ticket = 100;
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "-->开始售票了!");
while (true) {
// 同步代码块中的锁对象可以用this
// 因为子线程是实现了Runable接口
synchronized (this) {
if (ticket > 0) {
try {
Thread.sleep(200);
System.out.println(Thread.currentThread().getName() + "卖出第 " + (ticket--) + " 张票");
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
System.out.println(Thread.currentThread().getName() + "停止售票!!!!!!");
}
}
2.Test.java:
public class Test {
public static void main(String[] args) {
SellTicketThread stt = new SellTicketThread();
Thread t1 = new Thread(stt, "窗口1");
Thread t2 = new Thread(stt, "窗口2");
Thread t3 = new Thread(stt, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
2.同步方法
即有synchronized
关键字修饰的方法。
由于Java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
代码如:
public synchronized void go(){}
同步方法默认的锁对象就是this,所以普通的同步方法在Thread子类里面不能用。
静态同步方法,锁对象默认是当前类的类对象(类名点class) 。
所以在Thread子类里面想要用同步方法,那么,同步方法必须用static
修饰,并且锁对象用当前类的类对象。
在Runnable
实现类里面,同步方法可以不用加static
,直接用this
作为锁对象即可 。
注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。
电影院售票代码实例:
1.继承Thread实现例子代码:
1.SellTicketThread.java:
public class SellTicketThread extends Thread {
static int ticket = 100;
public SellTicketThread(String name) {
super(name);
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "-->开始售票了!");
while (true) {
boolean flag = sell();
if (!flag) {
break;
}
}
System.out.println(Thread.currentThread().getName() + "停止售票!!!!!!");
}
// Thread子类的同步方法必须用Static修饰
public static synchronized boolean sell() {
// 用来判断100张票是否卖完;卖完-->false;没卖完-->true
boolean flag = false;
if (ticket > 0) {
try {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "卖出第 " + (ticket--) + " 张票");
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
}
return flag;
}
}
2.Test.java:
public class Test {
public static void main(String[] args) {
SellTicketThread t1 = new SellTicketThread("窗口1");
SellTicketThread t2 = new SellTicketThread("窗口2");
SellTicketThread t3 = new SellTicketThread("窗口3");
t1.start();
t2.start();
t3.start();
}
}
2.实现Runable接口例子代码:
演示代码如下:
1.SellTicketThread:
public class SellTicketThread implements Runnable {
// 无需Static修饰
int ticket = 100;
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "-->开始售票了!");
while (true) {
boolean flag = sell();
if (!flag) {
break;
}
}
System.out.println(Thread.currentThread().getName() + "停止售票!!!!!!");
}
// 无需Static修饰
public synchronized boolean sell() {
// 用来判断100张票是否卖完;卖完-->false;没卖完-->true
boolean flag = false;
if (ticket > 0) {
try {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "卖出第 " + (ticket--) + " 张票");
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
}
return flag;
}
}
2.Test.java:
public class Test {
public static void main(String[] args) {
SellTicketThread stt = new SellTicketThread();
Thread t1 = new Thread(stt, "窗口1");
Thread t2 = new Thread(stt, "窗口2");
Thread t3 = new Thread(stt, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
Lock锁同步
介绍
在JDK1.5中新增了一个java.util.concurrent
包来支持同步。
ReentrantLock
类是可重入、互斥、实现了Lock接口的锁,
它与使用synchronized
方法和``synchronized代码块具有相同的基本行为和语义,并且扩展了其能力。
ReenreantLock
类的常用方法有:
ReentrantLock()
: 创建一个ReentrantLock
实例
lock()
: 获得锁
unlock()
: 释放锁
注意:ReentrantLock()
还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用。
Lock锁跟同步代码块的区别::
1.同步代码块和同步方法:没有明显看出,在哪里加锁,在哪里释放锁。
而Lock锁:加锁解锁明显。
2.一旦程序有报错,同步代码块会自动释放锁;
lock锁不会自动释放锁,一旦进入到同步代码部分,报错终止,那么这个锁一辈子无法打开,其他线程根本进不去。
所以:我们的lock.unlock()
这句代码,必须执行,无论什么情况都得执行,必须必须执行!!!!
所以我们的lock
需要跟try finally
结合使用。
关于 Lock 对象和 synchronized 关键字的选择:
a.最好两个都不用,使用一种java.util.concurrent
包提供的机制,
能够帮助用户处理所有与锁相关的代码。
b.如果synchronized
关键字能满足用户的需求,就用synchronized
,因为它能简化代码
c.如果需要更高级的功能,就用ReentrantLock()
类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁。
try{
lock.lock();
//被同步的代码
}finally{
lock.unlock();
}
例如:
在上面例子的基础上,改写后的代码为:
1.继承Thread实现例子代码:
1.MyThread.java:
public class MyThread extends Thread {
public static int tickets = 100;
public static Lock myLock = new ReentrantLock();
public MyThread(String name) {
super(name);
}
@Override
public void run() {
System.out.println(getName() + "开始售票!");
while (true) {
// 加锁
myLock.lock();
try {
if (tickets > 0) {
try {
sleep(100);
System.out.println(getName() + "--->售出了第:" + (tickets--) + "张票!");
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println(getName() + "停止售票!!!!");
break;
}
} finally {
// 解锁
myLock.unlock();
}
}
}
}
2.Test.java:
public class Test {
public static void main(String[] args) {
MyThread t1 = new MyThread("窗口1");
MyThread t2 = new MyThread("窗口2");
MyThread t3 = new MyThread("窗口3");
t1.start();
t2.start();
t3.start();
}
}
2.实现Runable接口例子代码:
1.MyThread.java:
public class MyThread implements Runnable {
public int tickets = 100;
public Lock myLock = new ReentrantLock();
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始售票!");
while (true) {
// 加锁
myLock.lock();
try {
if (tickets > 0) {
try {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "--->售出了第:" + (tickets--) + "章票!");
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println(Thread.currentThread().getName() + "停止售票!!!!");
break;
}
} finally {
// 解锁
myLock.unlock();
}
}
}
}
2.Test.java:
public class Test {
public static void main(String[] args) {
MyThread t = new MyThread();
Thread t1 = new Thread(t, "窗口1");
Thread t2 = new Thread(t, "窗口2");
Thread t3 = new Thread(t, "窗口3");
t1.start();
t2.start();
t3.start();
}
}