新的一天,回顾一下昨天的问题!
首先从问题引入:
有三个窗口共同卖100张票,使用实现Runnable接口的方式
问题:在买票的过程中,出现了重票、错票。
原因:当一个线程在操作ticket过程中,操作尚未完成,其他线程参与进来,也操作车票---->出现了线程安全问题。
如何解决:当一个线程a操作ticket时,其他线程不能参与进来,直到a操作完ticket时,其他线程线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。
这就涉及到同步机制来解决线程安全问题:
- 同步代码块
- 同步方法
- Lock锁
弊端: 操作同步代码时,只能有一个线程参与,其他线程等待,相当于是一个单线程的过程,效率低。
一.同步代码块:
synchronized(同步监视器){
//需要被同步的代码
}
说明:
- 操作共享数据的代码,即为需要被同步的代码 -->不能包裹多了,也不能包裹少了
- 共享数据:多个线程共同操作的变量。比如: ticket就是共享数据。
- 同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。但使用时要特别小心。
要求:多个线程必须要共用同一把锁。
同步代码块解决买票问题
public class Test{
public static void main(String[] args) {
Window2 w = new Window2();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
class Window2 implements Runnable {
private int ticket = 100;
//private final Object obj = new Object();
@Override
public void run() {
while (true){
synchronized (this){//synchronized (obj){
if(ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" +ticket);
ticket--;
}else {
break;
}
}
}
}
}
这里提一下代码块包裹多与少的问题,如果将while(true){ }也包裹进去,那么一个线程就将票卖完了,因为只有当代码块中的代码执行完毕,线程才会放开锁。
二.同步方法:
结合同步代码块的理解,如果将操作共享数据的代码完整的声明在一个方法中,可以将方法声明为同步----加上synchronized关键字
总结:
- 同步方法仍然涉及到同步监视器,只是不需要显示的声明;
- 非静态同步方法,同步监视器为this;
静态的同步方法,同步监视器为 当前类本身,当一个类第一次加载的时候就唯一存在于内存中,通过xxx.class获得,这也是一个对象
三.创建多线程时-同步方法的同步监视器的补充说明
- 在实现Runable接口的方式中可以用this充当同步监视器
- 在继承Thread类创建多线程的方式中,慎用this充当同步监视器,可能不小心就创建了多个继承类,导致this不唯一。可以考虑(Xxx.class),这也是一个对象
- 关于第二条慎用的理解(不是说不用),请看下面代码----两个储户存一个账户。操作共享数据是Account类,作为Customer类的成员,能保证唯一性。
class Account{
private int balance;
public Account(int balance){
this.balance = balance;
}
public synchronized void save(int money){
if(money > 0){
balance += money;
System.out.println(Thread.currentThread().getName() + "存钱成功,余额为:" + balance);
}
}
}
class Customer extends Thread{
private Account acct;
public Customer(Account acct){
this.acct = acct;
}
@Override
public void run(){
//分三次存三千
for(int i = 0;i < 3;i++){
acct.save(1000);
}
}
}
public class Test{
public static void main(String[] args) {
Account acct = new Account(0);
Customer C1 = new Customer(acct);
Customer C2 = new Customer(acct);
C1.setName("甲");
C2.setName("乙");
C1.start();
C2.start();
}
}
四.Lock锁:
JDK5.0新增的解决办法——通过显式定义同步锁来实现同步,锁由Lock对象充当,使用继承类ReentrantLock。具体步骤看代码注释。
问题: synchronized与Lock的异同?
相同:二者都可以解中线程安全问题
不同: synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
Lock需要手动的启动同步(Lock()) ,同时结束同步也需要手动的实现(unLock() )
Lock锁解决买票问题
import java.util.concurrent.locks.ReentrantLock;
class Window implements Runnable {
private int ticket = 100;
//1.实例化一个ReentrantLock
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
//2.调用锁定方法lock()
lock.lock();
if (ticket > 0) {
try {//加大出现重票、错票的概率
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
ticket--;
}else {
break;
}
}
finally {
//调用解锁方法unlock()
lock.unlock();
}
}
}
}
public class Lock {
public static void main(String[] args) {
Window w = new Window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
五.小结一下锁的释放问题
释放锁的操作:
●当前线程的同步方法、同步代码块执行结束。
●当前线程在同步代码块、同步方法中遇到break、return终止 了该代码块、该方法的继续执行。
●当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
●当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。
不会释放锁的操作:
●线程执行同步代码块或同步方法时,程序调用Thread .sleep()、Thread.yield()方法暂停当前线程的执行。
●线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。
➢应尽量避免使用suspend()和resume()来控制线程(过时的)。