多线程4
①.线程同步
并发:同一个对象被多个线程同时操作
上万人同时抢票,两个银行同时取钱
- 处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。这个时候我们就需要线程的同步。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用。
- 线程同步的形成条件:对象 + 锁,以保证线程的安全性
由于同意进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入 - 锁机制 - synchronized,当一个线程获得对象的排它锁,独占资源,其它线程必须等待,使用后释放锁即可。可能存在以下问题:
- 一个线程持有锁会导致其它所有需要此锁的线程挂起;
- 在多线程竞争下,加锁→释放锁 会导致比较多的上下文切换和调度延时,引起性能问题;
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题。
②.三大线程不安全案例
-抢票不安全的案例
多人(多线程)同时抢票,操作票数时会造成票数的错误
//不安全的买票
//线程不安全,可能会有复数,重复的抢票
public class UnsafeByTicket {
public static void main(String[] args) {
ByTicket station = new ByTicket(); //拿掉抢票类型的实列对象
//创建多个线程来同时操作这个对象
Thread user1 = new Thread(station,"小明");
Thread user2 = new Thread(station,"张三");
Thread user3 = new Thread(station,"王五");
Thread user4 = new Thread(station,"小李");
//开启现线程
user1.start();
user2.start();
user3.start();
user4.start();
}
}
class ByTicket implements Runnable{
//票数
private int ticket = 20;
boolean flag = true; //外部停止方式
@Override
public void run() {
//买票
while(flag){
buy();
}
System.out.println("票抢完了!");
}
//买票的方法
private void buy(){
//判断是否有票
if(ticket <= 0) {
flag = false;
return;
}
//买票
System.out.println(Thread.currentThread().getName() + " 抢到了第 " + ticket-- + " 张票");
new MySleep().Asleep(1000); //模拟网络延时
}
}
-取钱不安全的案例
两个对象同时操作一个数据,可能直接导致数据为负,造成损失
//不安全的取钱
//两个人去银行取钱
public class UnsafeBank {
public static void main(String[] args) {
//创建账户
Account account = new Account(100,"基金");
//创建两个线程,来进行取钱操作
Drawing you = new Drawing(account,50,"你自己");
Drawing wife = new Drawing(account,100,"你妻子");
you.start();
wife.start();
}
}
//账户
class Account {
double money; //账户余额
String name; //卡名
public Account(double money, String name) {
this.money = money;
this.name = name;
}
}
//银行:模拟取款
class Drawing extends Thread{
Account account; //需要有一个账户
double drawingMoney; //取了多少钱
double yourMoney; //拿在手里的钱
public Drawing(Account account,double drawingMoney,String name){
super(name); //调用父类的有参构造,传入线程的名字
this.account = account;
this.drawingMoney = drawingMoney;
}
@Override
public void run() {
//判断有没有钱
if(account.money - drawingMoney < 0){
System.out.println(Thread.currentThread().getName() + "账户余额不足!");
return;
}
//sleep可以放大问题的发生性
new MySleep().Asleep(1000); //模拟延迟,让两个人都能见到100的余额
account.money -= drawingMoney; //计算卡内余额
yourMoney += drawingMoney; //计算现在你手里的现金
System.out.println(account.name + "余额为:" + account.money);
System.out.println(this.getName() + "手里的钱:" + yourMoney);
}
}
列表不安全的案例
多线程操作列表中的元素,不安全
//数据不安全的集合
public class UnsafeList {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (int i = 0; i <= 10000; i++) {
new Thread(() -> { //创建10000个线程来将线程的名字写入列表
list.add(Thread.currentThread().getName());
},i + " ").start();
}
//new MySleep().Asleep(2000);
System.out.println("列表里一共有:" + list.size() + "条数据"); //输出的数据是不会达到10000的
//不同的线程有可能在同一瞬间往列表里添加数据,这样就导致了最终链表数据的不足
}
}
③.线程同步
同步方法:
- 就像我们可以通过private关键字来保证数据对象只能被方法访问一样,我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,他包括两种用法:synchronized方法和synchronized块
同步方法:public synchronized void method( int args ){ }- synchronized 方法控制对”对象“的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。
优点:一定程度上保证了线程的安全
缺点:若将一个大的方法声明为synchronized将会影响效率
注意:方法里边需要修改的内容才需要锁,锁太多会浪费资源- 同步块synchronized(Obj) { }
Obj称之为同步监视器
Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class[反射中的内容]
同步监视器的执行过程
第一个线程访问,锁定同步监视器,执行其中的代码。
第二个线程访问,发现同步监视器被锁定,无法访问。
第一个线程访问完毕,解锁同步监视器。
第二个线程访问,发现同步监视器没有锁,然后锁定并访问。
-抢票案例的优化
使用同步之后,没有再出现票数为负的状况,各个线程依次访问对象
//不安全的买票
//线程不安全,可能会有复数,重复的抢票
public class UnsafeByTicket {
public static void main(String[] args) {
···
}
}
class ByTicket implements Runnable{
//票数
private int ticket = 20;
boolean flag = true; //外部停止方式
@Override
public void run() {
//买票
····
}
//买票的方法 设置成同步方法,锁的是this(ByTicket类)
private synchronized void buy(){
//判断是否有票
if(ticket <= 0) {
flag = false;
return;
}
//买票
System.out.println(Thread.currentThread().getName() + " 抢到了第 " + ticket-- + " 张票");
new MySleep().Asleep(100); //模拟网络延时
}
}
-银行取钱案例的优化
二者同时看到余额后必须要排队才能进行取钱操作,如果余额不足是没有办法取钱的,避免了重复的操作
- 注意:synchronized方法默认锁的是this对象,当需要加锁的对象不在this方法里,就需要只用同步块来进行线程同步操作,括号中的同步监视器也就是需要同步的那一个对象。
- 一般加锁的对象是需要 进行 增、删、改 的对象
//取钱 Runnable 实现
public class UnsafeBank2 {
public static void main(String[] args) {
//获取账户的实列对象
Account2 account2 = new Account2(100,"小基金");
//创建两个线程来执行取钱的操作
Bank you = new Bank(account2,50);
Bank wife = new Bank(account2,100);
new Thread(you,"你自己").start();
new Thread(wife,"你老婆").start();
}
}
//银行账户类
class Account2{
double money;
String name;
public Account2(double money, String name) {
this.money = money;
this.name = name;
}
}
class Bank implements Runnable{
Account2 account2; //需要实例的取钱账户的对象
private double drawingMoney; //需要知道取了多少钱
private double handMoney; //可以返回你的手里有多少钱
public Bank(Account2 account2,double drawingMoney){ //Bank类的构造器,获取对象时就传入参数
this.account2 = account2;
this.drawingMoney = drawingMoney;
}
@Override
public void run() {
//synchronized默认锁的是this对象,当需要锁定其它对象的时候就需要使用同步块,括号中传入的是需要锁的对象名,即同步监视器
//锁的对象一定是需要 增、删、改 的量
synchronized (account2){
if(account2.money - drawingMoney < 0){
System.out.println(Thread.currentThread().getName() + "账户余额不足");
return;
}
new MySleep().Asleep(1000); //模拟网络延时
account2.money -= drawingMoney; //计算账户中的余额
handMoney += drawingMoney; //计算现在你手中的现金
System.out.println(Thread.currentThread().getName() + "取走了:" + drawingMoney);
System.out.println(account2.name + "余额:" + account2.money);
}
}
}
-列表的优化
//集合不安全的优化
public class UnsafeList {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (int i = 0; i < 10000; i++) {
new Thread(() -> { //创建10000个线程来将线程的名字写入列表
synchronized (list){ //用同步块将对象锁住
list.add(Thread.currentThread().getName());
}
},i + " ").start();
}
new MySleep().Asleep(2000); //这里不加sleep方法会导致主线程提前跑完
System.out.println("列表里一共有:" + list.size() + "条数据"); //输出:10000
}
}
-CopyOnWriteArrayList
JUC当中一个安全类型的集合
//测试JUC安全类型的集合
public class TestJUC {
public static void main(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
list.add(Thread.currentThread().getName());
}).start();
}
new MySleep().Asleep(1000);
System.out.println("列表里一共有:" + list.size() + "条数据");
}
}
④.死锁
多个线程各自占有一些共享资源,并且互相等待其它线程占有的资源才能运行,而导致两个或多个线程都在等待对方释放资源,都停止执行的情形。
在某一个同步块同时拥有“两个以上的对象的锁”时,就会发生死锁问题。
//死锁:多个线程互相持有对方需要的资源,形成僵持
public class DeadLock {
public static void main(String[] args) {
MakeUp girl1 = new MakeUp(0,"大姐");
MakeUp girl2 = new MakeUp(1,"二姐");
girl1.start();
girl2.start();
}
}
//口红
class Lipstick{}
//镜子
class Mirror{}
//化妆
class MakeUp extends Thread{
//化妆需要的资源只有一份 用static修饰
static Lipstick lipstick = new Lipstick();
static Mirror mirror = new Mirror();
int choice; //选择
String girlName; //化妆的人名
MakeUp(int choice,String girlName){
this.choice = choice;
this.girlName = girlName;
}
@Override
public void run() { //化妆
makeUp();
}
//互相持有对方的锁,需要拿到对方的资源
private void makeUp(){
if(choice == 0){
synchronized (lipstick){ //获得口红的锁
System.out.println(this.girlName + "获得了口红的锁");
new MySleep().Asleep(1000);
synchronized (mirror){ //一秒种后,在拿着口红的状态下,想获得镜子
System.out.println(this.girlName + "获得了镜子的锁");
}
}
}else{
synchronized (mirror){ //获得镜子的锁
System.out.println(this.girlName + "获得了镜子的锁");
new MySleep().Asleep(2000);
synchronized (lipstick){ //一秒种后,在拿着镜子的情况下,想获得口红
System.out.println(this.girlName + "获得了口红的锁");
}
}
}
}
}
产生死锁的四个必要条件条件:
互斥条件:一个资源每次只能被一个进程使用
请求互斥条件:一个进程因请求资源而阻塞时,对方已获得的资源保持不放
不剥夺条件:进程已经获得资源,在未使用完之前,不能强行剥夺
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
- 只要想办法破除了其中一个条件就可以避免死锁的发生