在多线程编程中,有可能会出现同时访问同一个资源的情况,这种资源可以是各种类型的的资源:一个变量、一个对象、一个文件、一个数据库表等,而当多个线程同时访问同一个资源的时候,就会存在一个问题:由于每个线程执行的过程是不可控的,所以很可能导致最终的结果与实际上的愿望相违背或者直接导致程序出错。
在售票员的案例中,多个线程访问的时候就会出现数据出错的情况
售票系统有两个个渠道, 网络购票,现场购票,下面模拟购票流程,然后我们启动两个线程代表网络和线程购票
我们定义两个个线程一起执行,
则会有可能出现数据错误, 如下图
这是因为当一个线程在还有一张票的时候进入到购票系统,但是在这儿cpu的时间段内没有走完程序,即票数没有-1, 然后cpu执行另一个线程,此时票数还是1,所以还会进入到购票流程,当这个线程执行完毕以后票数会变成0,cpu会调用第一次堵塞的线程, 此时票的数量为 0-1=-1 所以此时数据出错.
可以用线程同步的方式解决上面的数据异常方法,有三种方法,分别为同步代码块,同步方法 ,Lock
当线程遇到同步代码块或者同步方法的时候,会先判断同步锁(一个对象)是否存在,如果存在,则会将同步锁加到这个线程上,执行程序,(如果程序没有执行完同步代码块的方法则这个同步锁不会被释放) ,当另一个线程想要进入这个方法的时候会先判断一下同步锁是否存在,如果有,则进入执行,如果没有,则等待同步锁被释放,即保证了这个程序在某一时刻只能有一个线程去访问.
对象锁(同步锁) : 任意对象,如果多个线程需要对某一个对象保持同步,则这些线程的对象锁要相同,锁住的不是变量,而是操作变量的方法,一个对象只拥有一个锁.类本身也有锁.
synchronized(对象锁){
线程要操作的共享数据
}
synchronized(obj){
//对票数判断,大于0,可以出售,变量--操作
if( ticket > 0){
try{
Thread.sleep(10);
}catch(Exception ex){}
System.out.println(Thread.currentThread().getName()+" 出售第 "+ticket--);
}
}
同步方法的对象锁是当前对象 即 this
静态方法的对象锁为当前类.class
public synchronized void payTicket(){
if( ticket > 0){
try{
Thread.sleep(10);
}catch(Exception ex){}
System.out.println(Thread.currentThread().getName()+" 出售第 "+ticket--);
}
}
lock所更灵活,但是需要自己手动释放锁
示例
Lock ck = new ReentrantLock();
public void run(){
while(true){
ck.lock();
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
}
ck.unlock();
}
}
这三个方法是 java.lang.Object 的 final native 方法,任何继承 java.lang.Object 的类都有这三个方法。它们是Java语言提供的实现线程间阻塞和控制进程内调度的底层机制.
三个方法的解释:
- wait() :等待,将正在执行的线程释放其执行资格 和 执行权,并存储到线程池中。
- notify():唤醒,唤醒线程池中被wait()的线程,一次唤醒一个,而且是任意的。
- notifyAll(): 唤醒全部:可以将线程池中的所有wait() 线程都唤醒。
其实,所谓唤醒的意思就是让 线程池中的线程具备执行资格。必须注意的是,这些方法都是在 同步中才有效。同时这些方法在使用时必须标明所属锁,这样才可以明确出这些方法操作的到底是哪个锁上的线程。
通过输入输出来演示等待和唤醒
有两个线程 input线程的作用是输入数据到对象,output作用是从对象中输出数据, 然后要求一次输入一次输出
**基本过程**
- 输入:赋值后,执行方法wait()等待,并且改变Resource的Tag值,唤醒输出
- 输出:被唤醒后,判断Tag,如果不进入wait() 然后输出,改变Tag的值 , 输出完毕以后自己wait() 唤醒输入 notify()
**注意** :1. 要给输入和输出同一个锁才能起到同步的作用 2. wait()和notify()需要用锁对象来调用,这样才知道唤醒或者休眠那个锁中的线程
**程序**
```
main:
public class ThreadDemo{
public static void main(String[] args) {
Resource r = new Resource();
Input in = new Input(r); // 要给输入和输出同一个锁才能起到同步的作用
Output out = new Output(r);
Thread tin = new Thread(in);
Thread tout = new Thread(out);
tin.start();
tout.start();
}
}
public class Resource {
public String name;
public String sex;
public boolean flag = false;
}
/*
* 输入的线程,对资源对象Resource中成员变量赋值
* 一次赋值 张三,男
* 下一次赋值 lisi,nv
*/
public class Input implements Runnable {
private Resource r ;
public Input(Resource r){
this.r = r;
}
public void run() {
int i = 0 ;
while(true){
synchronized(r){
//标记是true,等待
if(r.flag){
try{r.wait();}catch(Exception ex){}
}
if(i%2==0){
r.name = "张三";
r.sex = "男";
}else{
r.name = "lisi";
r.sex = "nv";
}
//将对方线程唤醒,标记改为true
r.flag = true;
r.notify(); // wait()和notify()需要用锁对象来调用,这样才知道唤醒或者休眠那个锁中的线程
}
i++;
}
}
}
/*
* 输出线程,对资源对象Resource中成员变量,输出值
*/
public class Output implements Runnable {
private Resource r ;
public Output(Resource r){
this.r = r;
}
public void run() {
while(true){
synchronized(r){
//判断标记,是false,等待
if(!r.flag){
try{r.wait();}catch(Exception ex){}
}
System.out.println(r.name+".."+r.sex);
//标记改成false,唤醒对方线程
r.flag = false;
r.notify();
}
}
}
}
```
通过
当线程任务中出现了多个同步(多个锁) 时,如果同步中嵌套了其他的同步。这时容易引发一种现象:程序出现无限等待,这种现象我们称为死锁。这种情况能避免就避免掉。其中同步锁要是唯一锁(即整个程序中只有一个这种锁)
两个线程互相持有对象在等待的东西
死锁的四个必要条件
public class DeadLock implements Runnable{
private int i = 0;
public void run(){
while(true){
if(i%2==0){
//先进入A同步,再进入B同步
synchronized(LockA.locka){
System.out.println("if...locka");
synchronized(LockB.lockb){
System.out.println("if...lockb");
}
}
}else{
//先进入B同步,再进入A同步
synchronized(LockB.lockb){
System.out.println("else...lockb");
synchronized(LockA.locka){
System.out.println("else...locka");
}
}
}
i++;
}
}
}
public class DeadLockDemo {
public static void main(String[] args) {
DeadLock dead = new DeadLock();
Thread t0 = new Thread(dead);
Thread t1 = new Thread(dead);
t0.start();
t1.start();
}
}
public class LockA {
private LockA(){}
public static final LockA locka = new LockA();
}
public class LockB {
private LockB(){}
public static final LockB lockb = new LockB();
}