线程安全问题
需求:某电影院目前正在上映国产大片,共100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
public class MyThread extends Thread{
//表示这个类的所有对象都共享ticket
static int ticket=0; //0~99
@Override
public void run(){
while(true){
if(ticket<100){
try{
Thread.sleep(100);
}catch(InterruptedException e){
e.printStackTrace();
}
ticket++:
System.out.println(getName()+"正在卖第"+ticket+"张票");
}else{
break;
}
}
}
}
public static void main(String[] args){
//创建线程对象
MyThread t1 = new Thread();
MyThread t2 = new Thread();
MyThread t3 = new Thread();
//起名字
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
//开启线程
t1.start();
t2.start();
t3.start();
}
此时的代码会有一个问题,3个窗口的票数不是多了就是重复了
多个线程操作同一个数据时,就会产生问题
问题1:相同的票出现了多次
问题2:出现了超出范围的票
原因:线程执行时,有随机性,多个线程操作同一个共享变量
如果线程执行到ticket++;还没有打印,cpu的执行权就被抢走了,就可能会打印相同的数据
//本地内存->主内存
可以用同步代码块解决
同步代码块:把操作共享数据的代码锁起来
格式
synchronized(锁对象){
操作共享数据的代码
}
//锁对象没有要求,但是要确保唯一
//例如static Object obj =new Object();
特点:默认锁打开,有一个线程进去了,锁自动关闭
特点2:里面的代码全部执行完毕,线程出来,锁自动打开
解决后的代码
public class MyThread extends Thread{
//表示这个类的所有对象,都共享ticket数据
static int ticket =0;
//锁对象,一定要是唯一的
static Object obj = new Object();
@Override
public void run(){
while(true){
synchronized(obj){
if(ticket<100){
try{
Thread.sleep(10);
}catch(InterruptedException e){
e.printStackTrace();
}
ticket++:
System.out.println(getName()+"正在卖第"+ticket+"张票");
}else{
break;
}
}
}
}
}
同步代码中的小细节
1.写在循环里面
2.锁对象,一定要是唯一的
一般书写为当前类的字节码文件
格式:类名.class
同步方法
把synchronized关键字加到方法上
格式:
修饰符 synchronized 返回值类型 方法名(方法参数){...}
特点1:同步方法时锁住方法里面的所有代码
特点2:锁对象不能自己指定
非静态:this
静态:当前类的字节码文件对象 类型.class
技巧:同步代码块完成后->写到同步方法
public class ThreadDemo{
public static void main(String[] args){
/*需求:
*100张票,3个窗口卖票 利用同步方法
*技巧: 同步代码块
*/
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
Thread t3 = new Thread(mr);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
public class MyRunnable implements Runnable{
int ticket=0;
@Override
public void run(){
//1.循环
//2.同步代码块(同步方法)
//3.判断共享数据是否到了末尾,如果到了末尾
//4.判断共享数据是否到了末尾,如果没有到末尾
while(true){
synchronized(MyRunnable.class){
if(ticket==100){
break;
}else{
try{
Thread.sleep(10);
}catch(InterruptedException e){
e.printStackTrace();
}
ticket++;
System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票");
}
}
}
}
}
//选中然后ctrl+alt+m就可以抽取成一个方法
//抽取方法后
public class MyRunnable implements Runnable{
int kicket=0;
@Override
public void run(){
//1.循环
while(true){
//2.同步方法
if(method())break;
}
}
//非静态方法 锁对象是this
privare synchronized boolean method(){
//3.判断共享数据是否到了末尾,如果到了末尾
if(ticket==100){
return true;
}else{
//4.判断共享数据是否到了末尾,如果没有到末尾
try{
Thread.sleep(10);
}catch(InterruptedException e){
e.printStackTrace();
}
ticket++;
System.out.println(Thread.currentThread().getName()+"在卖第"+ticket+"张票");
}
}
return false;
}
StringBuild和StringBuffer的方法是一样的
但是StringBuffer的方法都加上了synchronized都是同步方法
如果单线程可以用StringBuild,多线程用StringBuffer
Lock锁
虽然可以理解同步代码块和同步方法的锁对象问题
但是并没有直接看到锁
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
Lock实现提供比使用synchronized方法和语句,可以获得更广泛的锁定操作
Lock中提供了获得锁和释放锁的方法
void lock() 获得锁
void unlock() 释放锁
手动上锁,手动释放锁
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
ReentrantLock的构造方法
ReentrantLock() 创建一个RenntrantLock的实例
public class MyThread extends Thread{
static int ticket = 0;
static Lock lock = new ReentrantLock();
@Override
public void run(){
//1.循环
while(true){
//2.同步代码块
//synchronized(MyThread.class)
lock.lock();
//3.判断
try{
Thread.sleep(10);
}catch(InterruptedException e){
e.printStackTrace();
}
ticket++;
System.out.println(getName()+"正在卖第"+ticket+"张票");
}
lock.unlock();
}
}
public class ThreadDemo{
public static void main(String[] args){
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
此时还会有一个问题,虽然票卖完了,但是程序不会停止
原因是第100次之后结束循环后,锁还没有关
解决办法用try{...}catch{...}finally{关锁}
finally一定会被执行
更新后的代码
public class MyThread extends Thread{
static int ticket = 0;
static Lock lock = new ReentrantLock();
@Override
public void run(){
//1.循环
while(true){
//2.同步代码块
lock.lock();
try{
//3.判断
if(ticket==100){
break;
}else{
//4.判断
Thread.sleep(10);
ticket++;
System.out.println(getName()+"在卖第"+ticket+"张票");
}
}catch(InterruptedException e){
e.printStackTrace();
}finally{
lock.unlock();
}
}
}
}
死锁(应该避免的错误)
死锁:指的是两个或多个线程无限期地互相等待对方所持有的资源,导致程序无法继续执行下去的状态
1.大锁套小锁可以解决死锁的循环,但是性能更低
2.最好的办法就是不要嵌套两个锁