线程同步
定义
线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,处于等待状态,直到该线程完成操作, 其他线程才能对该内存地址进行操作。
举例卖票(模拟这类情况不能继承Thread,thread类的任务和线程是绑到一起的,开启一个线程,任务也会重新开启一个)
用卖票的例子,来模拟出现的一些问题,创建一个类,继承Thread类,在run方法中写买票的逻辑,创建多个线程,模拟买票的窗口。
卖票逻辑
package com.sj.thread;
public class ThreadDemo4 implements Runnable {
//定义总票数为100张
private Integer tickets=100;
//在run方法中实现卖票的具体逻辑
/*
1.先判断票数是否大于0,然后进行卖票的操作。
2.把票的总数目进行减一。
3.票卖完了可能还有人来咨询,设置一个死循环。
*/
@Override
public void run() {
while (true) {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
tickets--;
}
}
}
}
创建多个线程模拟卖票
package com.sj.thread;
public class ThreadDemo4Test {
public static void main(String[] args) {
ThreadDemo4 td=new ThreadDemo4();
//创建三个线程,模仿三个窗口买票
Thread thread1 = new Thread(td);
thread1.setName("窗口1");
Thread thread2 = new Thread(td);
thread2.setName("窗口2");
Thread thread3 = new Thread(td);
thread3.setName("窗口3");
//启动线程
thread1.start();
thread2.start();
thread3.start();
}
}
/*
输出结果:
窗口1正在出售第100张票
窗口2正在出售第100张票
窗口3正在出售第98张票
窗口2正在出售第97张票
窗口1正在出售第97张票
窗口3正在出售第95张票
窗口1正在出售第94张票
窗口2正在出售第94张票
窗口3正在出售第92张票
...
...
窗口2正在出售第7张票
窗口1正在出售第7张票
窗口3正在出售第5张票
窗口2正在出售第4张票
窗口1正在出售第4张票
窗口3正在出售第2张票
窗口1正在出售第1张票
窗口2正在出售第0张票
窗口3正在出售第-1张票
*/
结论
看打印结果,同一张票被卖了多次,如果多次测试,还会出现负数的情况,这是不符合实际情况的。
问题分析
为了问题更加明显,我们在判断票数大于零后,让线程休眠100毫秒,模拟出票的延迟。
package com.sj.thread;
public class ThreadDemo4 implements Runnable{
//定义总票数为100张
private Integer tickets=100;
@Override
public void run() {
while (true) {
//tickets 等于100
//t1,t2,t3 代表三个线程
//假设t1有cpu的执行权,判断票数大于0
if (tickets > 0) {
try {
//休息100好秒模拟出票延迟
Thread.sleep(100);
//t1休眠了100毫秒
//假设t2拿到cpu的执行权,当t2执行到这里的时候,它也休息100毫秒.
//此时t3拿到cpu的执行权,当t3执行到这里的时候,它也休息100毫秒。
} catch (InterruptedException e) {
e.printStackTrace();
}
//假设t1,t2,t3按顺序醒过来
//t1抢到cpu执行权,控制台打印窗口1正在出售第100张票
//t2抢到cpu执行权,控制台打印窗口2正在出售第100张票
//t3抢到cpu执行权,控制台打印窗口3正在出售第100张票
System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
//假设走到这里还是按顺序来的话,减减就会执行三次,最终票就会变成97 理解卖票数为负数的情 况的话,把100换成1在捋一遍。
tickets--;
}
}
}
}
上述问题就是多线程环境下的数据安全问题之一
判断是否会出现数据安全问题色条件
1,是否是多线程环境(多个窗口)
2,是否有共享数据(共同卖100张票)
3,是否有多条语句操作共享数据(总票数减减,如果只是拿出来看一看,多少个人看都不会变,就不会出现问题)
解决办法
1.使用同步代码块。格式如下
synchronized(任意对象){
操作共享数据的多条语句
}
package com.sj.thread;
public class ThreadDemo4 implements Runnable {
//定义总票数为100张
private Integer tickets = 100;
//任意对象做锁
Object obj = new Object();
//在run方法中实现卖票的具体逻辑
@Override
public void run() {
while (true) {
synchronized (obj) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
tickets--;
}
}
}
}
}
测试
package com.sj.thread;
public class ThreadDemo4Test {
public static void main(String[] args) {
ThreadDemo4 td=new ThreadDemo4();
//创建三个线程,模仿三个窗口买票
Thread thread1 = new Thread(td);
thread1.setName("窗口1");
Thread thread2 = new Thread(td);
thread2.setName("窗口2");
Thread thread3 = new Thread(td);
thread3.setName("窗口3");
//启动线程
thread1.start();
thread2.start();
thread3.start();
}
}
/*
输出结果:
窗口3正在出售第100张票
窗口3正在出售第99张票
窗口1正在出售第98张票
窗口1正在出售第97张票
窗口1正在出售第96张票
...
...
窗口3正在出售第6张票
窗口1正在出售第5张票
窗口1正在出售第4张票
窗口1正在出售第3张票
窗口3正在出售第2张票
窗口3正在出售第1张票
*/
2.同步方法(就是在普通方法返回值前加synchronized关键字)
package com.sj.thread;
public class ThreadDemo5 implements Runnable {
//定义总票数为100张
private Integer tickets = 100;
//在run方法中调用同步方法
//在同步方法中写具体的操作逻辑
@Override
public void run() {
while (true) {
sellTickets();
}
}
private synchronized void sellTickets() {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
tickets--;
}
}
}
测试
package com.sj.thread;
public class ThreadDemo4Test {
public static void main(String[] args) {
ThreadDemo5 td=new ThreadDemo5();
//创建三个线程,模仿三个窗口买票
Thread thread1 = new Thread(td);
thread1.setName("窗口1");
Thread thread2 = new Thread(td);
thread2.setName("窗口2");
Thread thread3 = new Thread(td);
thread3.setName("窗口3");
//启动线程
thread1.start();
thread2.start();
thread3.start();
}
}
/*
输出结果:
窗口3正在出售第100张票
窗口3正在出售第99张票
窗口3正在出售第98张票
窗口3正在出售第97张票
窗口3正在出售第96张票
窗口3正在出售第95张票
窗口3正在出售第94张票
窗口3正在出售第93张票
...
...
窗口2正在出售第9张票
窗口2正在出售第8张票
窗口2正在出售第7张票
窗口2正在出售第6张票
窗口2正在出售第5张票
窗口2正在出售第4张票
窗口2正在出售第3张票
窗口2正在出售第2张票
窗口2正在出售第1张票
*/
3.使用lock锁(明确加锁,解锁)
package com.sj.thread;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadDemo6 implements Runnable {
//定义总票数为100张
private Integer tickets = 100;
//先new一把锁
Lock lock = new ReentrantLock();
//在run方法中实现卖票的具体逻辑
@Override
public void run() {
while (true) {
lock.lock();
if (tickets > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
tickets--;
lock.unlock();
}else{
//防止由于死循环,最后上锁等不到解锁,报错
lock.unlock();
}
}
}
}
测试
package com.sj.thread;
public class ThreadDemo4Test {
public static void main(String[] args) {
ThreadDemo6 td=new ThreadDemo6();
//创建三个线程,模仿三个窗口买票
Thread thread1 = new Thread(td);
thread1.setName("窗口1");
Thread thread2 = new Thread(td);
thread2.setName("窗口2");
Thread thread3 = new Thread(td);
thread3.setName("窗口3");
//启动线程
thread1.start();
thread2.start();
thread3.start();
}
}
/*
输出结果:
窗口3正在出售第100张票
窗口3正在出售第99张票
窗口3正在出售第98张票
窗口3正在出售第97张票
窗口3正在出售第96张票
窗口3正在出售第95张票
窗口3正在出售第94张票
窗口3正在出售第93张票
...
...
窗口2正在出售第5张票
窗口2正在出售第4张票
窗口2正在出售第3张票
窗口2正在出售第2张票
窗口2正在出售第1张票
*/