什么是线程安全?
当多个线程同时对同一资源访问,并且写的时候,可能会受到其它线程干扰,导致数据问题,这种现象被称之为线程安全问题。(多线程读的时候不会产生线程安全问题)
线程不安全例子
/**
* 线程不安全例子
* @author terry
* @date 2018年5月26日
*/
public class ThreadUnsafe {
//导致线程安全
public static void main(String[] args) {
SellTicket ticket = new SellTicket();
for (int i = 0;i<50;i++) {
BuyTicket buy = new BuyTicket(ticket);
buy.start();
}
}
}
/**
* 窗口卖票
*/
class SellTicket{
public int count = 100;//
public void doSell(){
count--;
}
}
/**
* 黄牛买票
*/
class BuyTicket extends Thread{
SellTicket ticket;
public BuyTicket(SellTicket ticket) {
this.ticket = ticket;
}
@Override
public void run() {
while(ticket.count > 0){
System.out.println("黄牛【"+getName()+"】买了一张票,当前票数:"+ticket.count);
ticket.doSell();
}
}
}
可以看到下面多处数据问题,概率还是很大的
黄牛【Thread-0】买了一张票,当前票数:100
黄牛【Thread-3】买了一张票,当前票数:100
黄牛【Thread-1】买了一张票,当前票数:100
黄牛【Thread-2】买了一张票,当前票数:100
黄牛【Thread-4】买了一张票,当前票数:98
黄牛【Thread-4】买了一张票,当前票数:96
黄牛【Thread-4】买了一张票,当前票数:95
黄牛【Thread-4】买了一张票,当前票数:94
黄牛【Thread-4】买了一张票,当前票数:93
黄牛【Thread-4】买了一张票,当前票数:92
黄牛【Thread-4】买了一张票,当前票数:91
黄牛【Thread-4】买了一张票,当前票数:90
黄牛【Thread-4】买了一张票,当前票数:89
黄牛【Thread-5】买了一张票,当前票数:89
线程如何同步,保证数据的原子性。
解决方案:
-
synchronized 特点:自动挡
-
lock jdk1.5(并发包中) 特点:手动锁(需要手动开启和关闭)
使用synchronized解决线程安全问题
public class ThreadSyn {
//导致线程安全
public static void main(String[] args) {
SellTicket ticket = new SellTicket();
for (int i = 0;i<5;i++) {
BuyTicket buy = new BuyTicket(ticket);
buy.start();
}
}
}
/**
* 窗口卖票
*/
class SellTicket{
public int count = 100;//
}
/**
* 黄牛买票
*/
class BuyTicket extends Thread{
SellTicket ticket;
public BuyTicket(SellTicket ticket) {
this.ticket = ticket;
}
@Override
public void run() {
while(ticket.count > 0){
doSell();
}
}
//写法一:同步代码块
public void doSell(){
synchronized (ticket) {//只要有线程持有该把锁,其它线程就不会进来
if (ticket.count <= 0) {
return;
}
System.out.println("黄牛【"+getName()+"】买了一张票,当前票数:"+ticket.count);
ticket.count--;
}
}
//写法二:同步函数,一般推荐同步代码块,锁住的区域越多,效率越慢(相当于单线程了)
public synchronized void doSell1(){//当前写法存在线程安全问题,稍后解释
if (ticket.count <= 0) {
return;
}
System.out.println("黄牛【"+getName()+"】买了一张票,当前票数:"+ticket.count);
ticket.count--;
}
}
同步代码块执行结果:
黄牛【Thread-4】买了一张票,当前票数:11
黄牛【Thread-4】买了一张票,当前票数:10
黄牛【Thread-4】买了一张票,当前票数:9
黄牛【Thread-4】买了一张票,当前票数:8
黄牛【Thread-4】买了一张票,当前票数:7
黄牛【Thread-4】买了一张票,当前票数:6
黄牛【Thread-4】买了一张票,当前票数:5
黄牛【Thread-4】买了一张票,当前票数:4
黄牛【Thread-4】买了一张票,当前票数:3
黄牛【Thread-4】买了一张票,当前票数:2
黄牛【Thread-4】买了一张票,当前票数:1
改成调用同步函数:
黄牛【Thread-4】买了一张票,当前票数:3
黄牛【Thread-4】买了一张票,当前票数:2
黄牛【Thread-4】买了一张票,当前票数:1
黄牛【Thread-0】买了一张票,当前票数:21
黄牛【Thread-2】买了一张票,当前票数:22
黄牛【Thread-3】买了一张票,当前票数:7
黄牛【Thread-1】买了一张票,当前票数:13
可以看到出现线程问题了,注意:同步函数的锁对象是this,每次线程进行判断的时候拿的不是同一把锁。
解决方法:
- 方案一:只创建一个BuyTicket对象,那么this对象就是同一把锁了。
public class ThreadSyn {
//导致线程安全
public static void main(String[] args) {
SellTicket ticket = new SellTicket();
BuyTicket buy = new BuyTicket(ticket);
for (int i = 0;i<5;i++) {
Thread thread = new Thread(buy);
thread.start();
}
}
}
- 方案二:使用静态同步函数,那么锁对象就是class文件了
public class ThreadSyn {
//导致线程安全
public static void main(String[] args) {
SellTicket ticket = new SellTicket();
for (int i = 0;i<5;i++) {
BuyTicket buy = new BuyTicket(ticket);
buy.start();
}
}
}
/**
* 窗口卖票
*/
class SellTicket{
public int count = 100;//
}
/**
* 黄牛买票
*/
class BuyTicket extends Thread{
static SellTicket ticket;
public BuyTicket(SellTicket ticket) {
this.ticket = ticket;
}
@Override
public void run() {
while(ticket.count > 0){
doSell1();
}
}
public void doSell(){
synchronized (ticket) {//只要有线程持有该把锁,其它线程就不会进来
if (ticket.count <= 0) {
return;
}
System.out.println("黄牛【"+getName()+"】买了一张票,当前票数:"+ticket.count);
ticket.count--;
}
}
public synchronized static void doSell1(){
if (ticket.count <= 0) {
return;
}
System.out.println("黄牛【"+Thread.currentThread().getName()+"】买了一张票,当前票数:"+ticket.count);
ticket.count--;
}
}
注意细节:
- 有两条线程或以上的线程,可能会产生线程安全的地方。
- 线程必须持有同一把锁(比如:class,局部变量,或者线程拿到的一样值。。),才能保证同步块一条线程执行
- 锁对象:可以为任意对象(必须保证唯一)
- 线程调用sleep方法,是不会释放锁对象的
原理:
- 当一个线程拿到锁后,其它线程就要等待该线程执行完毕后再执行。
- 锁释放,当代码执行完毕或者程序抛出异常,都会释放锁。
**缺点:**加锁效率会变慢