多线程——02

线程优先级

  • Java提供了一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定调度那个线程来执行。
  • 线程的优先级用数字来表示,范围从1~10
    • Thread.MIN_PRIORITY=1;
    • Thread.MAX_PRIORITY=10;
    • Thread.NORM_PRIORITY=5;
  • 使用以下方式改变或获取优先级
    • getPriority().setPriority( int xxx)

优先级的设定建议在start()调度前

注意:优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用了,这都是看CPU的调度。

测试:

//测试线程的优先级
public class TestPriority {
    public static void main(String[] args) {
        //打印主线程默认优先级
        System.out.println(Thread.currentThread().getName()+"主线程---->优先级:"+Thread.currentThread().getPriority());
        //创建线程对象
        MyPriority myPriority=new MyPriority();
        Thread t1 = new Thread(myPriority, "线程1");
        Thread t2 = new Thread(myPriority, "线程2");
        Thread t3 = new Thread(myPriority, "线程3");
        Thread t4 = new Thread(myPriority, "线程4");
        Thread t5 = new Thread(myPriority, "线程5");
        Thread t6 = new Thread(myPriority, "线程6");
        //先设置优先级,在启动
        t1.start();
        t2.setPriority(1);
        t2.start();
        t3.setPriority(4);
        t3.start();
        t4.setPriority(Thread.MAX_PRIORITY); //MAX_PRIORITY=10
        t4.start();
        t5.setPriority(9);
        t5.start();
        t6.setPriority(8);
        t6.start();
    }
}

class MyPriority implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"---->优先级:"+Thread.currentThread().getPriority());
    }
}

结果:
main主线程---->优先级:5
线程1---->优先级:5
线程4---->优先级:10
线程5---->优先级:9
线程6---->优先级:8
线程3---->优先级:4
线程2---->优先级:1

Process finished with exit code 0

从执行结果可以看出,一般情况下是优先级高的线程先执行,但也有可能是优先级低的先执行,因为优先级只能提高线程被优先调度的概率,具体还得看CPU的调度。

守护线程daemon

  • 线程分为用户线程(如:main)和守护线程(如:垃圾回收GC,监控内存)
  • 虚拟机必须确保用户线程执行完毕
  • 虚拟机不用等待守护线程执行完毕
  • 一般线程都默认为用户线程

测试:

    //测试守护线程
    public class TestDaemmon {
        public static void main(String[] args) {
            Dog dog=new Dog();
            YOU you=new YOU();
            new Thread(dog).start();//用户线程
            Thread thread=new Thread(you);
            thread.setDaemon(true);//默认是false,标识用户线程,true标识守护线程
            thread.start();
        }
    } 
    class Dog implements Runnable{
        @Override
        public void run() {
            for (int i = 0; i < 15; i++) {
                System.out.println("A happy year");
            }
            System.out.println("--------Good Bye--------");
        }
    }
    class YOU implements Runnable{
        @Override
        public void run() {
            while(true){
                System.out.println("bless dog");
            }
        }
    }

结果:
bless dog
bless dog
bless dog
bless dog
A happy year
A happy year
A happy year
A happy year
A happy year
A happy year
A happy year
A happy year
A happy year
A happy year
A happy year
A happy year
A happy year
A happy year
A happy year
--------Good Bye--------
bless dog
bless dog
bless dog
bless dog
bless dog
bless dog

Process finished with exit code 0

从结果可以看出:当用户线程执行完死亡后,守护线程虽然是一个死循环结构,但不久后也结束执行,因为虚拟机只确保用户线程执行完毕不用等待守护线程也执行完毕。

线程同步

多个线程操作同一个资源

并发

—->同一个对象被多个线程同时操作

处理多线程问题时,多个线程访同一个对象,并且某些线程还修改了这个对象,这个时候我们就需要线程同步。线程同步其实是一种 等待机制 ,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕,下一个线程再使用。

队列和锁

线程同步的形成条件:队列+锁

由于同一个进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可。

存在以下问题:

  • 一个线程持有锁会导致其他需要此锁的线程挂起(降低性能);
  • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换调度延时,引起性能问题;
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题;

三大不安全案例

测试1:买票

package syn;

public class UnsafeBuyTickets {
    public static void main(String[] args) {
        BuyTicket station = new BuyTicket();
        new Thread(station,"线程1:小红").start();
        new Thread(station,"线程2:小兰").start();
        new Thread(station,"线程3:小明").start();
    }
    
}

class BuyTicket implements Runnable{
     //票数
    int ticketNums=10;
    //标志位
    boolean flag=true;
    @Override
    public void run() {
      //买票
       while(flag){
             try {
               Thread.sleep(1000);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
        buy();
       }
    }
    private void buy(){
        //判断是否有票
        if (ticketNums<=1){
            this.flag=false;
        }
        //买票
        System.out.println(Thread.currentThread().getName()+"拿到了第"+ticketNums+"票");
        ticketNums--;
    }
}
执行结果:
线程2:小兰拿到了第10票
线程2:小兰拿到了第9票
线程2:小兰拿到了第8票
线程2:小兰拿到了第7票
线程2:小兰拿到了第6票
线程2:小兰拿到了第5票
线程3:小明拿到了第10票
线程3:小明拿到了第3票
线程3:小明拿到了第2票
线程1:小红拿到了第10票
线程3:小明拿到了第1票
线程2:小兰拿到了第4Process finished with exit code 0

从结果可以看出, 线程不安全,数据紊乱 ,三个人同时拿到了第10票,还有人拿到第0票。

测试2:取钱

package syn;
//两人银行取钱
public class UnsafeBank {
    public static void main(String[] args) {
        Account account=new Account(100,"结婚基金");
        Drawing d1=new Drawing(account,50,"you");
        Drawing d2=new Drawing(account,100,"she");
        d1.start();
        d2.start();
    }
}
//账户
class Account{
    int money;//余额
    String name;//卡名
    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }
}
//银行:模拟取款
class Drawing extends Thread{
    Account account;//账户
    //取了多少钱
    int drawingMoney;
    //现在多少钱
    int nowMoney;
    public Drawing(Account account,int 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;
        }
        //延时,放大问题的发生性
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //卡内余额=余额-取的钱
        account.money=account.money-drawingMoney;
        //到手的钱
        nowMoney=nowMoney+drawingMoney;
        System.out.println(account.name+"余额为:"+account.money);
        System.out.println(this.getName()+"到手的钱:"+nowMoney);
    }
}
结果:
结婚基金余额为:50
you到手的钱:50
结婚基金余额为:-50
she到手的钱:100

Process finished with exit code 0

从结果可以看出, 线程不安全,数据错误,两人取出的钱数大于账户余额。

测试3:集合

import java.util.ArrayList;
import java.util.List;
public class UnsafeLIst {
    public static void main(String[] args) throws InterruptedException {
        List<String> list=new ArrayList<String>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                //10000此循环,每次开启一个线程,将线程名写入list集合,理论会写入10000个
               list.add(Thread.currentThread().getName());
            }).start();
        }
        Thread.sleep(3000);
        System.out.println(list.size());
    }
}

结果:
9999
Process finished with exit code 0

结果写入了9999个,缺一个

同步方法

由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,它有两种用法:synchronized方法和synchronized块。

同步方法:public synchronized void method(int args){ }

synchronized方法控制对”对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。

缺陷若将一个大的方法声明为synchronized将会影响效率。

方法里面需要修改的内容才需要锁,锁的太多,浪费资源。

同步块

同步块:synchronized (Obj){}

Obj称之为同步监视器也就是要锁住的对象

  • Obj可以是任何对象,但是推荐使用共享资源作为同步监视器

  • 同步方法中无需指定同步监视器,因为同步方法的监视就是this,就是这个对象本身,或者是class

同步监视器的执行过程:

  • 第一个线程访问,锁定同步监视器,执行其中代码
  • 第二个线程访问,发现同步监视器被锁定,无法访问
  • 第一个线程访问完毕,解锁同步监视器
  • 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
  • ………..

测试买票案例:使用同步方法

我们只需要给买票方法buy()加关键字 synchronized,是其变为同步方法此时锁的是 BuyTicket类本身

// synchronized同步方法,锁的是this,当前对象
    private synchronized void buy(){
        //判断是否有票
        if (ticketNums<=1){
            this.flag=false;
        }
        //买票
  System.out.println(Thread.currentThread().getName()+"拿到了第"+ticketNums+"票");
        ticketNums--;
    }

执行结果:

线程1:小红拿到了第10票
线程3:小明拿到了第9票
线程2:小兰拿到了第8票
线程2:小兰拿到了第7票
线程3:小明拿到了第6票
线程1:小红拿到了第5票
线程1:小红拿到了第4票
线程3:小明拿到了第3票
线程2:小兰拿到了第2票
线程1:小红拿到了第1Process finished with exit code 0

测试取钱案例:使用同步块

对run()方法进行如下修改,锁定共享资源account

@Override
   public void run() {
       //同步块,锁定account
       synchronized (account){
           //判断有没有钱
           if (account.money-drawingMoney<0){
               System.out.println(Thread.currentThread().getName()+":钱不够,取不了");
               return;
           }
           //延时
           try {
               Thread.sleep(1000);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           //卡内余额=余额-取的钱
           account.money=account.money-drawingMoney;
           //到手的钱
           nowMoney=nowMoney+drawingMoney;
           System.out.println(account.name+"余额为:"+account.money);
           System.out.println(this.getName()+"到手的钱:"+nowMoney);
       }
       }
结果:

结婚基金余额为:50
you到手的钱:50
she:钱不够,取不了

Process finished with exit code 0

线程you在执行时,同步监视器account被锁定,只有线程you执行完,才会释放锁,因此在you线程未执行完时,线程she无法访问account

测试集合案例:使用同步块

import java.util.ArrayList;
import java.util.List;
public class UnsafeLIst {
    public static void main(String[] args) throws InterruptedException {
        List<String> list=new ArrayList<String>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                //10000此循环,每次开启一个线程,将线程名写入list集合,理论会写入10000个
                synchronized (list){//锁定list
                    list.add(Thread.currentThread().getName());
                }

            }).start();
        }
        Thread.sleep(3000);
        System.out.println(list.size());
    }
结果:
10000

Process finished with exit code 0

Lock(锁)

  • 从JDK5.0开始,Java提供了更强大的线程同步机制——-通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当。
  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
  • ReentrantLock(可重入锁)类实现了Lock,它拥有synchronized同样的并发性和内存意义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁,释放锁。
用法:
    class A{
       private final ReentrantLock lock=new ReentrantLock();
        public void m(){
            lock.lock();//加锁
            try{
                //保证线程安全的代码
            }
            finally{
                lock.unlock();//解锁
                //如果同步代码有异常,要将unlock()写入finally语句块
            }
        }  
    }

测试:使用lock锁来解决案例1买票的线程不安全问题

package syn;

import java.util.concurrent.locks.ReentrantLock;

public class UnsafeBuyTickets {
    public static void main(String[] args) {
        BuyTicket station = new BuyTicket();
        new Thread(station,"线程1:小红").start();
        new Thread(station,"线程2:小兰").start();
        new Thread(station,"线程3:小明").start();
    }   
}

class BuyTicket implements Runnable{
     //票数
    int ticketNums=10;
    //标志位
    boolean flag=true;
    //定义lock锁-----------------
    private final ReentrantLock lock=new ReentrantLock();
    @Override
    public void run() {
        //买票
        lock.lock();//加锁--------------
        try{
            while(flag){
                buy();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }finally{
            lock.unlock();//解锁-------------------
        }

    }

    private void buy(){
        //判断是否有票
        if (ticketNums<=1){
            this.flag=false;
        }
        //买票
        System.out.println(Thread.currentThread().getName()+"拿到了第"+ticketNums+"票");
        ticketNums--;
    }
}
结果:
线程1:小红拿到了第10票
线程1:小红拿到了第9票
线程1:小红拿到了第8票
线程1:小红拿到了第7票
线程1:小红拿到了第6票
线程1:小红拿到了第5票
线程1:小红拿到了第4票
线程1:小红拿到了第3票
线程1:小红拿到了第2票
线程1:小红拿到了第1票

Process finished with exit code 0

synchronized与Lock的对比:

  • Lock是显示锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放。
  • Lock只有代码块锁,synchronized代码块锁(同步块)和方法锁(同步方法)。
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多子类)
  • 优先使用顺序:
    • Lock > 同步代码块>同步方法

死锁

多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。某一个同步块同时拥有 “两个以上对象的锁” 时,就会发生死锁现象。

测试:

package syn;
//死锁:多个线程互相锁着对方需要的资源,然后形成僵持
public class DeadLock {
    public static void main(String[] args) {
        Boy boy1=new Boy(0,"小明");
        Boy boy2=new Boy(1,"小王");
        boy1.start();
        boy2.start();
    }
}
//篮球
class Basketball{}
//足球
class Football{}
class Boy extends Thread{
   //需要的资源篮球和足球只有一份,用static来保证只有一份
    static Basketball basketball=new Basketball();
    static Football football=new Football();
    //选择 要玩的球类
    int choice;
    String name;
    Boy(int choice,String name){
        this.choice=choice;
        this.name=name;
    }
    private void play() throws InterruptedException {
       if (choice==0){
           synchronized (basketball){//获得篮球的锁
               System.out.println(this.name+"获得篮球的锁");
               Thread.sleep(1000);
               synchronized (football){//一秒获得足球的锁
                   System.out.println(this.name+"获得足球的锁");
               }
           }
       }
       else {
           synchronized (football){//获得足球的锁
               System.out.println(this.name+"获得足球的锁");
               Thread.sleep(1000);
               synchronized (basketball){//一秒获得篮球的锁
                   System.out.println(this.name+"获得篮球的锁");
               }
           }
       }
    }
    @Override
    public void run() {
        try {
            play();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
结果:
小明获得篮球的锁
小王获得足球的锁

------------》》》》》从结果可以看出,发生死锁现象程序卡死,因为上述同步块中都有两个锁:
synchronized (basketball){//获得篮球的锁
               System.out.println(this.name+"获得篮球的锁");
               Thread.sleep(1000);
               synchronized (football){//一秒获得足球的锁
                   System.out.println(this.name+"获得足球的锁");
               }

小明先获得篮球的锁,然后小王又获得足球的锁,小明的下步执行是要在有篮球的锁的情况下获得足球的锁,而小王的下步执行是有足球的锁的情况下获得篮球的锁,所以僵持住了,程序卡死。

要想解决上述问题,只需将同步块中的第二个锁放在方法块外面:

如:

private void play() throws InterruptedException {
       if (choice==0){
           synchronized (basketball){//获得篮球的锁
               System.out.println(this.name+"获得篮球的锁");
               Thread.sleep(1000);
               }
           
            synchronized (football){//一秒获得足球的锁
                   System.out.println(this.name+"获得足球的锁");
                
           }
       }
       else {
           synchronized (football){//获得足球的锁
               System.out.println(this.name+"获得足球的锁");
               Thread.sleep(1000);
               }
           
           synchronized (basketball){//一秒获得篮球的锁
                   System.out.println(this.name+"获得篮球的锁");
               
           }
       }
    }

结果:

小明获得篮球的锁
小王获得足球的锁
小明获得足球的锁
小王获得篮球的锁

Process finished with exit code 0

从结果可以看出,程序正常运行结束:小明先获得篮球的锁,然后小王又获得足球的锁,然后小明释放篮球的锁获得足球的锁,小王释放足球的锁获得篮球的锁。

避免死锁的方法

死锁产生的四个必要条件:

  • 互斥条件:一个资源每次只能被一个进程使用
  • 请求和保持关系:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:进程已获得的资源,在未使用之前,不能强行剥夺。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

上述列出了死锁的四个必要条件,我们只要想办法破其中的任意一个或多个条件就可以避免死锁发生。