线程的安全问题
错票情况:
不是说没加sleep就不会出错,程序本身就有缺陷,只是出错的概率低而已,加了阻塞只是使他的错误概率提高了。
-
卖票过程中,出现了重票、错票 --------------> 出现了线程安全问题
-
问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
-
解决:当一个线程a在操作ticket的时候,其他线程不可以参与进来,直到线程a操作完才可以参与,这种情况即使线程a出现了阻塞也不能被改变(安全)
-
在Java中,通过同步机制来解决线程的安全问题!
1. 同步代码块处理安全问题
-
synchronized(同步监视器){
// 需要被同步的代码
}
操作共享数据的代码,即为需要被同步的代码(不能包含多了,也不能包含少了!!!!!!!!!!!!)
共享数据:多个线程共同操作的变量,比如:卖票中的ticket
同步监视器:俗称:锁 🔒 任何一个类的对象,都可以充当锁,可以用this放进去,但要保证this唯一,甚至可以用xiancheng.class也行,因为类也是对象,并且只会加载一次,不管用谁,一定要保证 🔒 的唯一性!
要求:多个线程必须共用同一把锁 🔒
同步的方式:
- 优点:解决了线程的安全问题
- 缺点:操作同步代码的时候,只能有一个线程参与,其他线程等待,相当于是一个单线程的过程,效率低
实现runnable接口:可考虑使用this充当监视器
package com.ran;
public class ran {
public static void main(String[] args) {
xiancheng t=new xiancheng();
Thread t1=new Thread(t);
Thread t2=new Thread(t);
Thread t3=new Thread(t);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
//创建一个实现了runnable接口的类
class xiancheng implements Runnable{
private int ticket=100;
Object obj=new Object();
@Override
public void run() {
while (true){
synchronized(this){ //此时的this:唯一的xiancheng的对象 方式二: synchronized(obj)
if(ticket>0){
System.out.println(Thread.currentThread().getName()+" 卖票,票号为"+ticket);
ticket--;
}else {
break;
}
}
}
}
}
继承Thread类:慎用this,考虑使用当前类充当监视器
package com.ran;
public class ran {
public static void main(String[] args) throws InterruptedException {
duoxiancheng t1=new duoxiancheng();
duoxiancheng t2=new duoxiancheng();
duoxiancheng t3=new duoxiancheng();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class duoxiancheng extends Thread{
private static int ticket=100;
@Override
public void run() {
while (true){
// 不可用this,此时的this表示t1,t2,t3,不是同一个锁
synchronized (duoxiancheng.class){
if(ticket>0){
System.out.println(getName()+" 卖票,票号为" + ticket);
ticket--;
}else {
break;
}
}
}
}
}
2. 同步方法处理安全问题
如果操作共享数据的代码完整地声明在一个方法中,我们不妨将此方法声明为同步的
在需要同步的方法上加一个synchronized即可
实现runnable接口:
package com.ran;
public class ran {
public static void main(String[] args) {
xiancheng t=new xiancheng();
Thread t1=new Thread(t);
Thread t2=new Thread(t);
Thread t3=new Thread(t);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
//创建一个实现了runnable接口的类
class xiancheng implements Runnable{
private int ticket=100;
@Override
public void run() {
while (true){
show();
}
}
private synchronized void show(){ //同步监视器:this
if(ticket>0){
System.out.println(Thread.currentThread().getName()+" 卖票,票号为"+ticket);
ticket--;
}
}
}
继承Thread类:(切记==+Static==)
package com.ran;
public class ran {
public static void main(String[] args) throws InterruptedException {
duoxiancheng t1=new duoxiancheng();
duoxiancheng t2=new duoxiancheng();
duoxiancheng t3=new duoxiancheng();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class duoxiancheng extends Thread{
private static int ticket=100;
@Override
public void run() {
while (true){
show();
}
}
private static synchronized void show(){ //同步监视器:duoxiangcheng.class
// private synchronized void show(){此种方式是错误的,有三个监视器t1,t2,t3
if(ticket>0){
System.out.println(Thread.currentThread().getName()+" 卖票,票号为" + ticket);
ticket--;
}
}
}
3. 同步方法的总结
- 同步方法仍然涉及到同步监视器,只是不需要我们显式声明
- 非静态的同步方法,同步监视器是:this
- 静态的同步方法,同步监视器是:当前类本身
4. 死锁
package com.ran;
public class ran {
public static void main(String[] args) throws InterruptedException {
StringBuffer s1= new StringBuffer();
StringBuffer s2= new StringBuffer();
new Thread(){
@Override
public void run() {
synchronized (s1){
s1.append("a");
s2.append("1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2){
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s2){
s1.append("c");
s2.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1){
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
}
我们使用同步时,要避免出现死锁!
5. Lock锁处理线程安全问题
package com.ran;
import java.util.concurrent.locks.ReentrantLock;
public class ran1 {
public static void main(String[] args) {
xiancheng t=new xiancheng();
Thread t1=new Thread(t);
Thread t2=new Thread(t);
Thread t3=new Thread(t);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
//创建一个实现了runnable接口的类
class xiancheng implements Runnable{
private int ticket=100;
private ReentrantLock lock=new ReentrantLock();
Object obj=new Object();
@Override
public void run() {
while (true){
try {
lock.lock();
if(ticket>0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 卖票,票号为"+ticket);
ticket--;
}else {
break;
}
}finally {
lock.unlock();
}
}
}
}
- 在继承Thread类创建多线程加Lock锁的时候也切记要加 static,不然就是三把锁了!
6. 安全问题处理方式异同
synchronized处理方式和Lock处理方式的异同:
同:
- 二者都可以解决线程安全问题
异:
- synchronized机制在执行完相应的同步代码之后,自动释放同步监视器
- Lock需要手动启动同步( lock() ),结束同步也需要手动实现( unlock() )
7. 同步机制练习
甲乙两个人存钱,每次存1000,存三次,存完要打印一次
package com.ran;
import java.util.concurrent.locks.ReentrantLock;
public class ran {
public static void main(String[] args) {
Account account=new Account(0);
Customer c1 = new Customer(account);
Customer c2 = new Customer(account);
c1.setName("甲");
c2.setName("乙");
c1.start();
c2.start();
}
}
class Customer extends Thread{
private Account account;
private static ReentrantLock lock;
public Customer(Account account) {
this.account = account;
}
@Override
public void run() {
for (int i = 0; i <30; i++) {
account.deposit(1000);
}
}
}
class Account{
private double balance;
private static ReentrantLock lock=new ReentrantLock();
public Account(double balance) {
this.balance = balance;
}
public void deposit(double amt){
try {
lock.lock();
if(amt>0){
balance+=amt;
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"存钱成功,余额为"+balance);
}
}finally {
lock.unlock();
}
}
}