线程安全问题
一 为什么要进行线程同步?
java允许多线程并发控制,当多个线程同时操作一个可共享资源变量时(如对其进行增删改查操作),会导致数据不准确,而且相互之间产生冲突。所以加入同步锁以避免该线程在没有完成操作前被其他线程调用,从而保证该变量的唯一性和准确性。
二 使用synchronized关键字解决线程安全问题
共享数据,多个线程共同操作的变量,比如买票问题中的tickets就是共享数据。
同步监视器就是锁,锁必须是这几个线程共享的,唯一的。当t1线程拿到这个锁时,t2t3线程就拿不到了。
1.同步代码块
synchronized(同步监视器)
{需要同步的代码}
1.1
继承Thread类的方法,使用同步代码块解决线程不安全,慎用this。因为继承方式是造了三个Thread类的对象,那就对应了三把锁。
synchronized (obj),可以在类中构造静态对象保证唯一性
synchronized(Thread.class),可以直接使用类名作为锁
1.2
Runnable接口的方法,因为Thread类的唯一性,锁比较灵活。
例如:synchronized (this)
2.同步方法(同步函数)
2.1
继承Thread类的方法,使用同步方法解决线程不安全
同步方法依然需要使用同步监视器,只是不需要显示的声明
1.非静态的同步方法:同步监视器相当于this
2.静态的同步方法:同步监视器相当于类本身,类.class
————————————————————————————————————————
private static synchronized void run()
假如直接对run()方法上锁,run()方法存在与Thread继承类中,实现时可能造多个对象不唯一,static相当于加了类锁,保证了唯一性
2.2
Runnable接口的方法,因为Thread类的唯一性,可以直接上锁
public synchronized void run()
三 练习
1.继承Thread类,同步代码块
/**
* @author Tony Stark
* @create 2022-08-24 21:58
* 继承Thread类的方法,使用同步代码块解决线程不安全,慎用this。
*
* 说白了,这把锁必须是这几个线程共享的,唯一的。当t1线程拿到这个锁时,t2t3线程就拿不到了
* 如果这里使用this,因为继承方式是造了三个Thread类的对象,那就对应了三把锁。
*
*synchronized(同步监视器)
* {需要同步的代码}
* 共享数据,多个线程共同操作的变量,比如tickets就是共享数据
* 同步监视器:就是锁。任何一个类的对象都可以充当锁。
*/
class MyThread extends Thread{
private static int tickets = 100;
private static Object obj = new Object();
@Override
public void run(){
while(true){
synchronized (obj){
//方法二:synchronized (Thread.class) {
//这里不能使用this。可以使用类名,或者将一个static的obj对象作为锁
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售票,票号 " + tickets);
tickets--;
} else {
break;
}
}
}
}
}
public class window1 {
public static void main(String[] args){
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.setName("t1");
t2.setName("t2");
t3.setName("t3");
t1.start();
t2.start();
t3.start();
}
}
2.继承Thread类,同步方法
/**
* @author Tony Stark
* @create 2022-08-24 22:34
* 继承Thread类的方法,使用同步方法解决线程不安全
*
* 同步方法依然需要使用同步监视器,只是不需要显示的声明
* 1.非静态的同步方法:同步监视器相当于this
* 2.静态的同步方法:同步监视器相当于类本身,类.class
*/
class MyThread2 extends Thread{
private static int tickets = 100;
@Override
public void run() {
while (true) {
show();//这里要注意,不是不能把run()写成synchronized方法,而是这里不能把run方法里面的while包进去
}
}
private static synchronized void show(){
//继承的时候如果使用同步方法,需要把方法设置为静态
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售票,票号 " + tickets);
tickets--;
}
}
}
public class window2 {
public static void main(String[] args){
MyThread2 t1 = new MyThread2();
MyThread2 t2 = new MyThread2();
MyThread2 t3 = new MyThread2();
t1.setName("t1");
t2.setName("t2");
t3.setName("t3");
t1.start();
t2.start();
t3.start();
}
}
3.继承Runnable接口,使用同步代码块和同步方法
/**
* @author Tony Stark
* @create 2022-08-23 8:23
* 静态方法是类锁,不论创建了几个对象,类锁只有一把
* 继承接口的方法,使用同步代码块和同步方法两种解决办法解决线程安全问题
*/
class Window implements Runnable{
private int tickets = 100;
@Override
public synchronized void run(){//这等同于同步代码块,同步监视器是this
while(true){
//while如果在代码块里面,某一个线程抢到锁之后会把票买完再释放锁。
//synchronized (this) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售票"
+ ",票号为 " + tickets);
tickets--;
} else {
break;
}
// }
}
}
}
public class window3{
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();
}
}
四 实例:账户取钱问题
1.创建一个账户Account类(账号,余额,构造器,getset方法,取款withdraw方法)
2.创建AccountThread继承Thread,取款是多线程操作 (定义Account属性,构造器接收参数,重写run方法)
3.Test
线程安全:
1.Account类
public class Account{
private String acton;//账号
private double balance;
public Account(){
}
public Account(String acton,double balance){
this.acton = acton;
this.balance = balance;
}
public String getAct(){
return acton;
}
public void setAct(String acton){
this.acton = acton;
}
public double getBalance(){
return balance;
}
public void setBalance(double balance){
this.balance = balance;
}
Object obj = new Object();
Object obj2 = new Object();
/*Account类只有一个对象act,那么自然只有一个Object对象obj1或obj2
obj1或者obj2都是共享对象,当t1拿到锁,t2就不会进入synchronized锁住的代码
所以和this一样,obj1和obj2可作为参数传入synchronized()
*/
public void withdraw(double money){
Object obj3 = new Object();
//withdraw在run()方法中,t1线程会创建一个obj3对象,t2也会,所以obj3不是共享对象,不能作为参数
synchronized (obj) {
double before = this.getBalance();
double after = before - money;
//在这里模拟网络延迟,100%会出现问题
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//t1线程执行到了这里,还没有来得及执行这行更新余额的代码,t2就进入了withdraw方法
this.setBalance(after);
}
}
/*
实例变量:在堆中,堆只有一个
静态变量:在方法区中,方法区只有一个
局部变量:在栈中
局部变量与常量永远不会共享,堆和方法区都是多线程共享的,可嫩存在线程安全问题
*/
}
/**
* 这是用继承的方式创建的,只不过传入了一个参数Account
*/
public class AccountThread extends Thread{
private Account act;
public AccountThread(Account act){
//act对象只有一个,两个线程对这一个账户操作
this.act = act;
}
public void run(){
double money = 5000;
act.withdraw(money);
/*
这里也可以
synchronized(act){act.withdraw(money)}
但是效率更低,是act不能是this,AccountThread两个对象,对应两把锁
*/
System.out.println("线程"+ Thread.currentThread().getName()+
"账户"+act.getAct()+ " 取款成功,余额 "+act.getBalance());
}
}
public class Test {
public static void main(String args[]){
Account act = new Account("act-001",10000);
//下面将继承类实例化的时候使用了多态
Thread t1 = new AccountThread(act);
Thread t2 = new AccountThread(act);
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
}
}
五 解决线程安全问题的另一种方式:Lock锁
/**
* 解决线程安全问题的方式三:Lock锁 --- JDK5.0新增
*
* 1. 面试题:synchronized 与 Lock的异同?
* 相同:二者都可以解决线程安全问题
* 不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
* Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())
*
* 2.优先使用顺序:
* Lock 同步代码块(已经进入了方法体,分配了相应资源) 同步方法(在方法体之外)
*/
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 {
//3.调用解锁方法:unlock()
lock.unlock();
}
}
}
}
public class LockTest {
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();
}
}