栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

JAVA多线程

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

JAVA多线程

1. 线程概述 1.1 什么是进程
  • 进程是系统进行资源分配的基本单位,也是独立运行的基本单位。
  • 目前操作系统都是支持多进程,可以同时执行多个进程,通过进程ID区分。
  • 单核CPU在同一时刻,只能运行一个进程;宏观并行、微观串行。
1.2 什么是线程
  • 线程又称轻量级进程(Light Weight Process),它是进程内一个相对独立的、可调度的执行单元,也是CPU的基本调度单位。
  • 一个进程由一个或多个线程组成,彼此间完成不同的工作,同时执行,称为多线程,此处宏观并行、微观串行。
  • JAVA虚拟机是一个进程,当中默认包含主线程(main),可通过代码创建多个独立线程,与main并发执行。
1.3 进程和线程的区别
  1. 进程是操作系统资源分配的基本单位,而线程是CPU的基本调度单位。
  2. 一个程序运行后之后有一个进程。
  3. 一个进程可以包含多个线程,但是至少需要有一个线程,否则这个线程是没有意义的。
  4. 进程间不能共享数据段地址,但同进程的线程之间可以。
1.4 线程的组成

   任何一个线程都具有基本的组成部分:

  • CPU时间片:操作系统会为每个线程分配执行时间。
  • 运行数据:
    (1)堆空间:存储线程需要使用的对象,多个线程可以共享堆中的对象。
    (2)栈空间:存储线程需要使用的局部变量,每个线程都拥有独立的栈。
  • 线程的逻辑代码
1.5 线程的特点
  1. 线程抢占式执行:效率高。可防止单一线程长时间独占CPU。
  2. 在单核CPU中,宏观上同时执行,微观上顺序执行。
2. 线程的创建

   创建线程的三种方式:

  1. 继承Thread类,重写run方法。
  2. 实现Runnable接口。
  3. 实现Callable接口。
2.1 创建线程(一)

   继承Thread类,重写run方法。

public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("子线程--------------"+i);
        }
    }
}
public class TestThread {
    public static void main(String[] args) {
        //1.创建线程对象
        MyThread myThread = new MyThread();
        //2.启动子线程,不能调用run方法
        myThread.start();
        //3.主线程执行
        for (int i = 0; i < 50; i++) {
            System.out.println("主线程-------------------"+i);
        }
    }
}
主线程-------------------0
主线程-------------------1
主线程-------------------2
子线程--------------0
主线程-------------------3
子线程--------------1
主线程-------------------4
子线程--------------2

主线程和子线程都是交替执行的,并且是抢占式执行。

2.2 获取和修改线程名称 (1)获取线程ID和线程名称:
  1. 在Thread的子类中调用this.getId()或this.getName()。
  2. 使用Thread.currentThread().getId()和Thread.currentTread().getName()
public class MyThread extends Thread{
    @Override
    public void run() {
        for(int i=0;i<10;i++) {
            //第一种方法
            System.out.println("线程ID:"+this.getId()+" "+"线程名:"+this.getName()+" "+i);
            //第二种方法
            //System.out.println("线程ID:"+Thread.currentThread().getId()+" "+"线程名:"+Thread.currentThread().getName());
        }
    }
}
主线程-------------------0
线程ID:17 线程名:Thread-1 0
线程ID:16 线程名:Thread-0 0
线程ID:17 线程名:Thread-1 1
主线程-------------------1
线程ID:17 线程名:Thread-1 2
线程ID:16 线程名:Thread-0 1
线程ID:17 线程名:Thread-1 3
  • 第一种方法的线程类必须继承Thread父类,否则不能使用这两个方法。
  • 第二种方法调用的静态方法currentThread表示获取当前线程,哪个线程执行的当前代码就获取谁。
(2)修改线程名称
  1. 调用线程对象的setName()方法。
  2. 使用线程子类的构造方法赋值。
//使用setName方法
myThread.setName("子线程1");
myThread.start();
myThread2.setName("子线程2");
myThread2.start();
//使用构造方法
public class MyThread extends Thread{
	public MyThread() {		
	}
	public MyThread(String name) {
		super(name);
	}
	@Override
	public void run() {
		//略
	}
}
2.3 创建线程(二)

   实现Runnable接口。

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for(int i=0;i<10;i++) {
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}
public class TestRunnable {
    public static void main(String[] args) {
        //1.创建MyRunnable对象,表示线程要执行的功能
        MyRunnable myRunnable = new MyRunnable();
        //2.创建线程对象
        Thread thread1 = new Thread(myRunnable,"我的线程1");
        //3.启动线程
        thread1.start();
        for (int i = 0; i < 50; i++) {
            System.out.println("main=================");
        }

        //1.创建MyRunnable对象,匿名内部类
        Runnable runnable = new Runnable(){
            @Override
            public void run() {
                for(int i=0;i<10;i++) {
                    System.out.println(Thread.currentThread().getName()+":"+i);
                }
            }
        };
        //2.创建线程对象
        Thread thread2 = new Thread(runnable,"我的线程2");
        //3.启动线程
        thread2.start();
    }
}
2.4 创建线程(三)

   实现Callable接口,见6.4小节。

3. 线程的基本状态

   线程的基本状态可以分为:

  1. 初始状态
    当线程对象被创建(new)之后即为初始状态。

  2. 就绪状态
    线程对象调用start方法之后进入就绪状态,此时只要获得了处理器便可以立即执行。

  3. 运行状态
    获得处理器之后,则进入运行状态,直到所分配的时间片结束,然后继续进入就绪状态。

  4. 等待状态
    因为发生某种事情而无法继续执行下去,例如调用sleep方法时线程进入限期等待,因某线程调用join使当前线程进入无限期等待。下一节会提到这两个方法。

  5. 终止状态
    主线程(main)结束或者该线程的run方法结束则进入终止状态,并释放CPU。

4. 常用方法
  • public static void sleep(long millis)
    当前线程主动休眠millis毫秒。
public class SleepThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+"-------------");
            try {
                Thread.sleep(1000); //(主线程)每隔一秒打印一次
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
    }
}
  • public static void yield()
    当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片。
public class YieldThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+"----------------");//打印结果会更接近于交替打印。
            //主动放弃cpu
            Thread.yield();
        }
    }
}
  • public final void join()
    允许其他线程加入到当前线程中。当某线程调用该方法时,加入并阻塞当前线程,直到加入的线程执行完毕,当前线程才继续执行。
public class TestJoin {
    public static void main(String[] args) {
        JoinThread j1 = new JoinThread();
        j1.start();
        try {
            j1.join(); //加入到当前线程(主线程main),并阻塞当前线程,直到加入线程执行完毕
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //for 主线程
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName()+"======"+i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  • pubic final void setPriority(int newPriority)
    改变该线程的优先级,线程优先级为1-10,默认为5,数字越大,优先级越高。
public class TestPriority {
    public static void main(String[] args) {
        PriorityThread p1 = new PriorityThread();
        PriorityThread p2 = new PriorityThread();
        PriorityThread p3 = new PriorityThread();
        p1.setName("p1");
        p2.setName("p2");
        p3.setName("p3");
        //设置优先级
        p1.setPriority(1);
        p3.setPriority(10);
        //启动
        p1.start();
        p2.start();
        p3.start();
    }
}

线程优先级高,只是权重高,获得CPU调度的概率高,并不是一定排前面。

  • public final void setDaemon(boolean on)
    (1)如果参数为true,则标记该线程为守护线程。
    (2)在JAVA中线程有两类:用户线程(前台线程)、守护线程(后台线程)。
    (3)如果程序中所有用户线程都执行完毕了,守护线程会自动结束。
    (4)垃圾回收器线程属于守护线程。
public class DeamonThread extends Thread{
    @Override
    public void run() {
        for(int i=0;i<50;i++) {
            System.out.println(Thread.currentThread().getName()+":"+i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class TestDeamon {
    public static void main(String[] args) {
        //创建线程(默认前台线程)
        DeamonThread d1 = new DeamonThread();
        //设置线程为守护线程
        d1.setDaemon(true);
        d1.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("主线程-------------");
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
主线程-------------
Thread-0:0
主线程-------------
主线程-------------
Thread-0:1
主线程-------------
主线程-------------
Thread-0:2
主线程-------------
主线程-------------
主线程-------------
Thread-0:3
主线程-------------
主线程-------------
Thread-0:4

Process finished with exit code 0

当主线程执行完毕后,子线程只打印了4次,但因为前者的结束而结束。

5. 线程安全

  这里有一个线程安全问题:假设有A、B两个线程,他们都往一个数组中的index位置存入一个数据并且执行index+1。当这两个线程同时执行时,数组中存入的结果会是什么?

public class ThreadSafe {
    private static int index=0;
    public static void main(String[] args) throws Exception{
        String[] strings=new String[5];
        //存入hello
        Runnable runnableA=new Runnable() {
            @Override
            public void run() {
                strings[index]="hello";
                index++;
            }
        };
        //存入world
        Runnable runnableB=new Runnable() {
            @Override
            public void run() {
                strings[index]="world";
                index++;
            }
        };
        Thread A=new Thread(runnableA);
        Thread B=new Thread(runnableB);
        A.start();
        B.start();
        //加入主线程,用来阻塞主线程使最后的输出语句最后执行
        A.join();
        B.join();
        System.out.println(Arrays.toString(strings));
    }
}

  多次执行代码,发现得到的结果并不一致,有可能出现[hello, world, null, null, null],也有可能出现[world, null, null, null, null];以第二个结果为例,当线程A存入hello之后,CPU马上就被线程B所抢夺,B存入了world覆盖了A存入的hello,这之后才执行了各自的index++。

  多线程安全问题:当多线程并发访问临界资源时,如果破坏了原子操作,可能会造成数据不一致。

  • 临界资源:共享资源(对于同一个对象),一次仅允许一个线程使用,才可以保证其正确性。
  • 原子操作:不可分割的多步操作,被视为一个整体,其顺序和步骤不可打乱或缺省,比如上一段代码的存hello和存world应当被看成两个原子操作。

  JAVA中,在程序应用里要保证线程的安全性就需要用到同步代码块。

5.1 同步方式(1)
  • 同步代码块:
//对临界资源对象加锁
synchronized(临界资源对象){
    //代码(原子操作)
}

把上文存hello和存world两个临界区放进同步代码块中就可以保证输出结果不会出现覆盖的情况:

synchronized (strings) {
    strings[index]="hello";
    index++;
}		
synchronized (strings) {
    strings[index]="world";
    index++;
}				

注意:

  • 每个对象都有一个互斥锁标记,用来分配给线程的。
  • 只有拥有对象互斥锁标记的线程,才能进入对该对象加锁的同步代码块。
  • 线程退出同步代码块时,会释放相应的互斥锁标记。

银行卡存取案例:

public class BankCard {
    private double money;

    public double getMoney() {
        return money;
    }

    public void setMoney(double money) {
        this.money = money;
    }
}
public class TicketThread {
    public static void main(String[] args) {
        //1.创建银行卡
        BankCard card = new BankCard();
        //2.创建两个操作
        Runnable add = new Runnable() {
            @Override
            public void run() {
                synchronized (card){
                    for (int i = 0; i < 10; i++) {
                        card.setMoney(card.getMoney()+1000);
                        System.out.println(Thread.currentThread().getName()+"存了1000,余额是:"+card.getMoney());
                    }
                }
            }
        };
        Runnable sub = new Runnable() {
            @Override
            public void run() {
                synchronized (card){
                    for (int i = 0; i < 10; i++) {
                        if(card.getMoney()>1000){
                            card.setMoney(card.getMoney()-1000);
                            System.out.println(Thread.currentThread().getName()+"取了1000,余额是:"+card.getMoney());
                        }else {
                            System.out.println("余额不足,请存取!");
                            i--;
                        }
                    }
                }
            }
        };
        //3.创建两个线程对象
        Thread thread1 = new Thread(add, "小明");
        Thread thread2 = new Thread(add, "小红");
        thread1.start();
        thread2.start();
    }
}
小明存了1000,余额是:1000.0
小明存了1000,余额是:2000.0
小明存了1000,余额是:3000.0
小明存了1000,余额是:4000.0
小明存了1000,余额是:5000.0
小红取了1000,余额是:4000.0
小红取了1000,余额是:3000.0
小红取了1000,余额是:2000.0
小红取了1000,余额是:1000.0
小红取了1000,余额是:0.0
  • 线程的状态(阻塞)

      当线程访问临界区(同步块代码)时,如果没有拿到访问锁,便进入阻塞状态。
5.2 同步方式(2)

使用同步方法:

//对当前对象(this)加锁
synchronized 返回值类型 方法名称(形参列表){
    //代码(原子操作)
}
 // 同步方法
    public static synchronized void get(BankCard card) {
        for (int i = 0; i < 5; i++) {
            if(card.getMoney()>=1000){
                card.setMoney(card.getMoney()-1000);
                System.out.println(Thread.currentThread().getName()+"取了1000,余额是:"+card.getMoney());
            }else {
                System.out.println("余额不足,请存取!");
                i--;
            }
        }
    }

  在这个同步方法中,锁就是this当前对象;如果是静态类,那么锁就是类对象,就相当于在同步代码块的括号里写XXX.class,XXX代表当前的类。

注意:

  • 只有在调用包含同步代码块的方法,或者同步方法时,才需要对象的锁标记。临界区(互斥执行)才需要加锁。
  • 如调用不包含同步代码块的方法,或普通方法时,则不需要锁标记,可直接调用。

已知JDK中线程安全的类:

  • StringBuffer
  • Vector
  • Hashtable
  • 以上类中的公开方法,均为synchronized修饰的同步方法。
5.3 死锁

死锁:

  • 当第一个线程拥有A对象锁标记,并等待B对象锁标记,同时第二个线程拥有B对象锁标记,并等待A对象锁标记时,产生死锁。
  • 一个线程可以同时拥有多个对象的锁标记,当线程阻塞时,不会释放已经拥有的锁标记,由此可能造成死锁。

  通过一个小案例来演示死锁的产生,假如两个人A和B在桌子上同时吃饭,桌上只有一双筷子,当一个人拥有两根筷子的时候才能吃:

public class Chopsticks {
}
public class TestChopsticks {
    public static void main(String[] args) {
        //创建两个锁对象(两根筷子)
        Chopsticks chopsticks1=new Chopsticks();
        Chopsticks chopsticks2=new Chopsticks();

        Runnable A=new Runnable() {
            @Override
            public void run() {
                //持有第一根筷子
                synchronized (chopsticks1) {
                    System.out.println("A拿到了一根筷子。");
                    //持有第二根筷子
                    synchronized (chopsticks2) {
                        System.out.println("A拿到了两根筷子,开始恰饭。");
                    }
                }
            }
        };

        Runnable B=new Runnable() {
            @Override
            public void run() {
                //持有第一根筷子
                synchronized (chopsticks2) {
                    System.out.println("B拿到了一根筷子。");
                    //持有第二根筷子
                    synchronized (chopsticks1) {
                        System.out.println("B拿到了两根筷子,开始恰饭。");
                    }
                }
            }
        };

        new Thread(A).start();
        new Thread(B).start();
    }
}

运行之后程序进入死锁状态,并且无限期地等待下去:

//控制台打印(程序未结束)
B拿到了一根筷子。
A拿到了一根筷子。

可以通过sleep方式使其中一个线程休眠一小会,A(B)吃完B(A)再吃。

5.4 线程通信

  在5.2节中的第二个案例银行卡存取中,打印出了很多余额不足,取钱线程在银行卡里没钱时也在不停地取钱,为了让取钱线程只在银行卡里有钱时再取,就需要实现线程间通信。

等待:

  • public final void wait()
  • public final void wait(long timeout)
  • 必须在对obj加锁的同步代码块中调用。在一个线程中,调用obj.wait()时,此线程会释放其拥有的所有锁标记。同时此线程阻塞在obj的等待队列中。总而言之,就是释放锁,进入等待队列。

通知:

  • public final void notify()
    -public final void notifyAll()
  • 进入等待的线程需要其他线程调用该线程的通知方法来将其唤醒。

还是银行卡存取案例,此处应用线程通信再来演示:

public class BandCard {
    private double Money;
    //标志,true表示卡里有钱-可取,false表示无钱-可存
    boolean flag=false;	
    public synchronized void put(double money) throws InterruptedException {
        //有钱不用存
        if (flag) {
            //进入等待队列(锁.wait),同时释放锁和CPU
            this.wait();
        }
        this.Money+=money;
        System.out.println("你爸存了"+money+"元,卡里还剩"+this.Money+"元。");
        //存完之后卡里有钱
        flag=true;
        //唤醒取钱线程
        this.notify();
    }
    public synchronized void take(double money) throws InterruptedException {	
        //没钱不能取
        if (!flag) {
            this.wait();
        }
        this.Money-=money;
        System.out.println("你取了"+money+"元,卡里还剩"+this.Money+"元。");
        flag=false;
        //唤醒存钱线程
        this.notify();
    }
}
public class AddMoney implements Runnable{
    BandCard card;
    public AddMoney(BandCard bandCard) {
        card=bandCard;
    }
    @Override
    public void run() {
        //存10次
        for(int i=0;i<10;i++) {
            try {
                card.put(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class SubMoney implements Runnable{
    BandCard card;
    public SubMoney(BandCard bandCard) {
        card=bandCard;
    }
    @Override
    public void run() {
        //取10次
        for(int i=0;i<10;i++) {
            try {
                card.take(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class testBankCard {
    public static void main(String[] args) {
        //创建银行卡对象
        BandCard bandCard=new BandCard();
        //创建操作
        AddMoney addMoney=new AddMoney(bandCard);
        SubMoney subMoney=new SubMoney(bandCard);
        //创建线程对象并启动
        new Thread(addMoney).start();
        new Thread(subMoney).start();
    }
}

运行结果:

你爸存了200.0元,卡里还剩200.0元。
你取了200.0元,卡里还剩0.0元。
你爸存了200.0元,卡里还剩200.0元。
你取了200.0元,卡里还剩0.0元。
你爸存了200.0元,卡里还剩200.0元。
你取了200.0元,卡里还剩0.0元。
你爸存了200.0元,卡里还剩200.0元。
你取了200.0元,卡里还剩0.0元。
你爸存了200.0元,卡里还剩200.0元。
你取了200.0元,卡里还剩0.0元。
你爸存了200.0元,卡里还剩200.0元。
你取了200.0元,卡里还剩0.0元。
你爸存了200.0元,卡里还剩200.0元。
你取了200.0元,卡里还剩0.0元。
你爸存了200.0元,卡里还剩200.0元。
你取了200.0元,卡里还剩0.0元。
你爸存了200.0元,卡里还剩200.0元。
你取了200.0元,卡里还剩0.0元。
你爸存了200.0元,卡里还剩200.0元。
你取了200.0元,卡里还剩0.0元。

Process finished with exit code 0

多存多取问题:
  但是如果往代码中再加入两个线程,比如你妈担心你钱不够用,也给你存钱;你妹妹来找你玩,往你卡里取钱。这时候就出现问题了:

你爸存了200.0元,卡里还剩200.0元。
你妹取了200.0元,卡里还剩0.0元。
你妈存了200.0元,卡里还剩200.0元。
你妹取了200.0元,卡里还剩0.0元。
你取了200.0元,卡里还剩-200.0元。
......
你取了200.0元,卡里还剩-2000.0元。
你妈存了200.0元,卡里还剩-1800.0元。
你爸存了200.0元,卡里还剩-1600.0元。

出现余额负数,程序永久等待的状态。

  出现余额负数的原因是当“你”,“你妹”两个取钱线程都因为flag为false而进入等待队列时,然后“你妹”被存钱线程所唤醒,此时余额为200,被唤醒的“你妹”继续取钱,此时余额为0,然后唤醒“你”,注意,“你”被唤醒后接着从wait语句之后往下执行取钱操作,此时余额为-200。问题就出在“你”这里,“你”被唤醒后是继续往下执行的,并没有重新判断flag,解决办法很简单,将if改为while就可以了,如果flag为false被唤醒的你就会接着等待:

你爸存了200.0元,卡里还剩200.0元。
你取了200.0元,卡里还剩0.0元。
你妈存了200.0元,卡里还剩200.0元。
你取了200.0元,卡里还剩0.0元。

  余额负数的问题解决了,但是程序陷入永久等待的问题还没解决,分析下原因:

  1. 你爸存钱成功,flag为true,余额200;
  2. 你妈存钱失败,进入等待队列;(你妈)
  3. 你爸存钱失败,进入等待队列;(你妈,你爸)
  4. 你取钱成功,flag为false,唤醒你妈,余额为0;(你爸)
  5. 你妹取钱失败,进入等待队列;(你爸,你妹)
  6. 你取钱失败,进入等待队列;(你爸,你妹,你)
  7. 你妈存钱成功,flag为true,唤醒你,余额为200;(你爸,你妹)
  8. 你妈存钱失败,进入等待队列;(你爸,你妹,你妈)
  9. 你取钱成功,flag为false,唤醒你妹,余额为0;(你爸,你妈)
  10. 你妹取钱失败,进入等待队列;(你爸,你妈,你妹)
  11. 你取钱失败,进入等待队列;(你爸,你妈,你妹,你)

  至此四个线程全部进入等待状态,在没有别的线程将其唤醒的情况下将陷入无限期等待。原因出在第9步,如果取钱线程“你”唤醒的是存钱线程,那么程序就会正常执行。修改方式也很简单,将代码中notify方法改成notifyAll就可以了,一次唤醒所有线程。结果正常运行不再演示,这里说这么多主要是体会线程同步的一个过程。

6. 线程池 6.1 线程池概念

1.首先有关线程的使用会出现两个问题:

  • 线程是宝贵的内存资源、单个线程约占1MB空间,过多分配易造成内存溢出。
  • 频繁的创建及销毁线程会增加虚拟机回收频率、资源开销,造成性能下降。

2.基于如上的问题,出现了线程池:

  • 线程容器,可设定线程分配的数量。
  • 将预先创建的线程对象存入池中,并可重用线程池中的线程对象。
  • 避免频繁的创建和销毁。
6.2 线程池原理

  将任务提交给线程池,由线程池分配线程、运行任务,并在当前任务结束后复用线程。

6.3 创建线程池

常用的线程池接口的类(所在包java.util.concurrent)

  • Executor:线程池的顶级接口。

  • ExecutorService:线程池接口,可通过submit(Runnable task)提交任务代码。实现类:
    1.ThreadPoolExecutor
    2.ScheduledThreadPoolExecutor
    3.AbstractExecutorService

  • Executors工厂类:创建线程池的工具类。四种:
    1.创建固定线程个数的线程池。
    2.创建缓存线程池,由任务的多少决定。
    3.创建单线程池。
    4.创建调度线程池。调度:周期、定时执行。

public class Demo1 {
    public static void main(String[] args) {
        //1.1创建固定线程个数的线程池
        ExecutorService es = Executors.newFixedThreadPool(4);
        //1.2创建缓存线程池,线程个数由任务个数决定
        ExecutorService es1 = Executors.newCachedThreadPool();
        //1.3创建单线程池。
        ExecutorService es2 = Executors.newSingleThreadExecutor();
        //1.4创建调度线程池。调度:周期、定时执行。
        ScheduledExecutorService es3 = Executors.newScheduledThreadPool(4);
        //2.创建任务
        Runnable runnable = new Runnable() {
            private int ticket = 100;
            @Override
            public void run() {
                while (true) {
                    if (ticket <= 0) {
                        break;
                    }
                    System.out.println(Thread.currentThread().getName() + "买了第" + ticket + "张票");
                    ticket--;
                }
            }
        };
        //3.提交任务
        for (int i = 0; i < 4; i++) {
            es.submit(runnable);
        }
        for (int i = 0; i < 3; i++) {
            es1.submit(runnable);
        }
        //4.关闭线程池
        es.shutdown(); //等待所有任务执行完毕再关闭线程池
        //es.shutdownNow(); //不等立即关闭线程池
    }
}
6.4 Callable接口
public interface Callable{
    public V call() throws Exception;
}
  • JDK1.5加入,与Runnable接口类似,实现之后代表一个线程任务。
  • Callable具有泛型返回值、可以声明异常。

与Runnable接口的区别:
1.Callable接口中call方法有返回值,Runnable接口中run方法没有返回值。
2.Callable接口中call方法有声明异常,Runnable接口中run方法没有异常。

public class Demo1{
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //功能需求:使用Callable实现1-100求和
        //1.创建Callable对象
        Callable callable = new Callable() {
            @Override
            public Integer call() throws Exception {
                System.out.println(Thread.currentThread().getName()+"开始计算");
                int sum=0;
                for(int i=1;i<=100;i++) {
                    sum+=i;
                }
                return sum;
            }
        };
        //2.把Callable对象转换成可执行任务
        FutureTask task = new FutureTask<>(callable);

        //3.创建线程
        Thread thread = new Thread(task);

        //4.启动线程
        thread.start();

        //5.获取结果(等待call执行完毕,才会返回结果)
        Integer sum=task.get();
        System.out.println("结果是:"+sum);
    }
}
6.5 Callable结合线程池使用
public class Demo2 {
    public static void main(String[] args) throws Exception{
        //1.创建线程池
        ExecutorService es = Executors.newFixedThreadPool(1);
        //2.提交任务 Future表示需要执行任务的结果
        Future future = es.submit(new Callable() {
            @Override
            public Integer call() throws Exception {
                System.out.println(Thread.currentThread().getName() + "开始计算");
                int sum = 0;
                for (int i = 1; i <= 100; i++) {
                    sum += i;
                }
                return sum;
            }
        });
        //3.获取任务结果(等待任务执行完毕才会返回)
        System.out.println(future.get());
        //4.关闭线程池
        es.shutdown();
    }
}
6.6 Future接口
  • Future:表示将要完成任务的结果。

  演示一个案例:使用两个线程,并发计算1-50、51-100的和,再进行汇总统计。

public class Demo3 {
    public static void main(String[] args) throws Exception{
        //1.创建线程池
        ExecutorService es = Executors.newFixedThreadPool(2);
        //2.提交任务
        Future future1 = es.submit(new Callable() {
            @Override
            public Integer call() throws Exception {
                System.out.println(Thread.currentThread().getName() + "开始计算");
                int sum = 0;
                for (int i = 1; i <= 50; i++) {
                    sum += i;
                }
                System.out.println("1-51计算完毕");
                return sum;
            }
        });
        Future future2 = es.submit(new Callable() {
            @Override
            public Integer call() throws Exception {
                System.out.println(Thread.currentThread().getName() + "开始计算");
                int sum = 0;
                for (int i = 51; i <= 100; i++) {
                    sum += i;
                }
                System.out.println("51-100计算完毕");
                return sum;
            }
        });
        //3.获取结果
        int sum=future1.get()+future2.get();
        System.out.println("结果是"+sum);
        //4.关闭线程池
        es.shutdown();
    }
}
  • 表示ExecutorService.submit()所返回的状态结果,就是call的返回值。
  • 方法get()以阻塞形式等待Future中的异步处理结果(call的返回值)。
6.7 线程的同步和异步

同步(一条执行路径)
  形容一次方法调用,同步一旦开始,调用者必须等待该方法返回,才能继续。当主线程调用子线程执行任务时,必须等到子线程返回结果后才能继续。

异步(多条执行路径)
  形容一次方法调用,异步一旦开始就像是一次消息传递,调用者告知之后立刻返回。二者竞争时间片,并发执行。异步有多条执行路径。

7. Lock接口

  JDK1.5加入,与synchronized比较,不仅显示定义,而且结构更灵活。提供了更多实用性方法,功能更强大、性能更优越。
常用方法:

  • void lock:获取锁,如果锁被占用,当前线程则进入等待状态。
  • boolean tryLock():尝试获取锁(成功返回true,失败返回false,不阻塞)
  • void unlock():释放锁。
7.1 重入锁

  ReentrantLock: Lock接口的实现类,与synchronized一样具有互斥锁功能。
  所谓重入锁,是指一个线程拿到该锁后,还可以再次成功获取,而不会因为该锁已经被持有(尽管是自己所持有)而陷入等待状态(死锁)。之前说过的synchronized也是可重入锁。

1.数组添加元素案例:

public class MyList {
    //创建锁
    private Lock locker=new ReentrantLock();
    private String[] str={"A","B","","",""};
    private int count=2;
    public void add(String value){
        locker.lock();
        try {
            str[count]=value;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count++;
            System.out.println(Thread.currentThread().getName()+"添加了"+value);
        }finally {
            locker.unlock();
        }
    }
    public String[] getStr(){
        return str;
    }
}
public class TestMyList {
    public static void main(String[] args) throws Exception{
        MyList list = new MyList();
        Runnable runnable1 = new Runnable(){
            @Override
            public void run() {
                list.add("hello");
            }
        };
        Runnable runnable2 = new Runnable(){
            @Override
            public void run() {
                list.add("world");
            }
        };
        Thread thread1 = new Thread(runnable1);
        Thread thread2 = new Thread(runnable2);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(Arrays.toString(list.getStr()));
    }
}
Thread-0添加了hello
Thread-1添加了world
[A, B, hello, world, ]

Process finished with exit code 0

2.买票案例使用lock锁:

public class Ticket implements Runnable{
    private int ticket=100;
    private Lock lock=new ReentrantLock();
    @Override
    public void run() {
        while (true){
            lock.lock();
            try {
                if(ticket<=0){
                    break;
                }
                System.out.println(Thread.currentThread().getName()+"买了第"+ticket+"张票");
                ticket--;
            }finally {
                lock.unlock();
            }
        }
    }
}
public class TestTicket {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        ExecutorService es = Executors.newFixedThreadPool(4);
        for (int i = 0; i < 4; i++) {
            es.submit(ticket);
        }
        es.shutdown();
    }
}
7.2读写锁

ReentrantReadWriteLock:

  • 一种支持一写多读的同步锁,读写分离,可以分别分配读锁和写锁。
  • 支持多次分配读锁,使多个读操作可以并发执行。

互斥规则:

  • 写—写:互斥,一个线程在写的同时其他线程会被阻塞。
  • 读—写:互斥,读的时候不能写,写的时候不能读。
  • 读—读:不互斥、不阻塞。
  • 在读操作远远高于写操作的环境中,可在保证线程安全的情况下,提高运行效率。
//读写锁的使用
public class ReadWriteLock {
    //创建读写锁
    ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    ReentrantReadWriteLock.ReadLock readLock=rwl.readLock();
    ReentrantReadWriteLock.WriteLock writeLock =rwl.writeLock();
    private int value;
    //读方法
    public int getValue()throws Exception{
        readLock.lock();
        try {
            Thread.sleep(1000);//休眠一秒
            return value;
        }finally {
            readLock.unlock();
        }
    }
    //写方法
    public void setValue(int value)throws Exception{
        writeLock.lock();
        try {
            Thread.sleep(1000);//休眠一秒
            this.value=value;
        }finally {
            writeLock.unlock();
        }
    }
}
public class TestReadWriteLock {
    public static void main(String[] args) {
        ReadWriteLock rwLock=new ReadWriteLock();
        ExecutorService es = Executors.newFixedThreadPool(2);
        Runnable read = new Runnable() {

            @Override
            public void run() {
                try {
                    System.out.println(rwLock.getValue());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        };
        Runnable write = new Runnable() {

            @Override
            public void run() {
                try {
                    rwLock.setValue(new Random().nextInt(100));
                    System.out.println(rwLock.getValue());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        };

        for (int i = 0; i < 2; i++) {
            es.submit(write); //提交两次写的任务
        }
        for (int i = 0; i < 10; i++) {
            es.submit(read); //提交18次读的任务
        }
        es.shutdown();
    }
}

读写锁比互斥锁运行时间更短。

8. 线程安全集合

  下图蓝色的表示线程安全的集合,绿色表示现代开发中已经很少使用的线程安全的集合。

  • Collection体系集合

  • Map安全集合体系

  在多线程中使用线程不安全的集合会出现异常。在JDK1.5之前,可以使用Collections中的工具类方法。
Collections工具类中提供了多个可以获得线程安全集合的方法:

public static  Collection synchronizedCollection(Collection c)
public static  List synchronizedList(List list)
public static  Set synchronizedSet(Set s)
public static  Map synchronizedMap(Map m)
public static  SortedSet synchronizedSortedSet(SortedSet s)
public static  SortedMap synchronizedSortedMap(SortedMap)

以上为JDK1.2提供,接口单一、维护性高,但性能没有提升,均以synchronized实现。

public class TestCollectionsMethod {
    public static void main(String[] args) {
        //1.1使用ArrayList(直接使用2步骤报异常)
        ArrayList arrayList=new ArrayList<>();

        //1.2使用Collections中的线程安全方法转成线程安全的集合
        List synList= Collections.synchronizedList(arrayList);

        //1.3使用并发包里提供的集合
        //CopyOnWriteArrayList arrayList2=new CopyOnWriteArrayList();

        //2.创建线程
        for(int i=0;i<20;i++) {
            int temp=i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int j=0;j<10;j++) {
                        synList.add(Thread.currentThread().getName()+":"+temp);
                        System.out.println(synList.toString());
                    }
                }
            }).start();
        }
    }
}
8.1 CopyOnWriteArrayList集合
  • 线程安全的ArrayList,加强版的读写分离。
  • 写有锁,读无锁,读写之间不堵塞,优于读写锁。
  • 写入时,先copy一个容器副本、再添加新元素,最后替换引用。所以说它是用空间换安全的一种方式。
  • 使用ArrayList无异。
public class TestCopyOnWriteArrayList {
    public static void main(String[] args) {
        //1.创建集合
        CopyOnWriteArrayList list=new CopyOnWriteArrayList();
        //2.使用多线程操作
        ExecutorService eService= Executors.newFixedThreadPool(5);
        //3.提交任务
        for(int i=0;i<5;i++) {
            eService.submit(new Runnable() {
                @Override
                public void run() {
                    for(int j=0;j<10;j++) {
                        list.add(Thread.currentThread().getName()+"..."+new Random().nextInt(1000));
                    }
                }
            });
        }
        //4.关闭线程池
        eService.shutdown();
        //等所有线程都执行完毕
        while(!eService.isTerminated());
        //5.打印结果
        System.out.println("元素个数:"+list.size());
        for (String string : list) {
            System.out.println(string);
        }
    }
}
8.2 CopyOnWriteArrayList源码分析

此集合所使用的的锁lock是重入锁ReentrantLock。

final transient ReentrantLock lock = new ReentrantLock();

此集合实际存储的数组array。

private transient volatile Object[] array;

在上节中调用的无参构造方法创建的是一个空的数组。

public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}
final void setArray(Object[] a) {
    array = a;
}

add(E):添加元素是先把原来的数组copy到一个长度加1的新数组里,然后对新数组进行操作,最后再把新数组赋给原数组。这个操作上了锁。

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

remove(int):删除元素同样是copy原数组到一个长度减1的新数组里,然后对新数组进行操作,最后再把新数组赋给原数组。这个操作也上了锁。

public E remove(int index) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        E oldValue = get(elements, index);
        int numMoved = len - index - 1;
        if (numMoved == 0)
            setArray(Arrays.copyOf(elements, len - 1));
        else {
            Object[] newElements = new Object[len - 1];
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index + 1, newElements, index,
                             numMoved);
            setArray(newElements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}

有关数组修改的操作都上了锁,也就说写操作是互斥访问的。

有关读操作的代码都是直接进行了访问,没有上锁,也就是说在写的同时可以读。

private E get(Object[] a, int index) {
    return (E) a[index];
}
8.3 CopyOnWriteArraySet集合
  • 线程安全的Set,底层使用CopyOnWriteArrayList实现。
  • 唯一不同在于,使用addIfAbsent()添加元素,会遍历数组,如果已有元素(比较依据是equals),则不添加(扔掉副本)。
//演示CopyOnWriteArraySet的使用
public class TestCopyOnWriteArraySet {
    public static void main(String[] args) {
        //1.创建集合
        CopyOnWriteArraySet set=new CopyOnWriteArraySet();
        //2.添加元素
        set.add("tang");
        set.add("he");
        set.add("yu");
        set.add("wang");
        set.add("tang");//重复元素,添加失败
        //3.打印
        System.out.println(set.size());
        System.out.println(set.toString());
    }
}

8.4 CopyOnWriteArraySet源码分析

这个集合实际上使用的就是CopyOnWriteArrayList集合。

private final CopyOnWriteArrayList al

它的无参构造方法new的就是CopyOnWriteArrayList对象,所以它是有序的。

public CopyOnWriteArraySet() {
    al = new CopyOnWriteArrayList();
}

添加元素的操作和CopyOnWriteArrayList大同小异。

public boolean add(E e) {
    return al.addIfAbsent(e);
}
public boolean addIfAbsent(E e) {
    Object[] snapshot = getArray();
    return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
    addIfAbsent(e, snapshot);
}

这是一个三元表达式,意思是存在相同元素返回false,否则添加元素。

先进入indexOf方法查看源码:

private static int indexOf(Object o, Object[] elements,
                           int index, int fence) {
    if (o == null) {
        for (int i = index; i < fence; i++)
            if (elements[i] == null)
                return i;
    } else {
        for (int i = index; i < fence; i++)
            if (o.equals(elements[i]))
                return i;
    }
    return -1;
}

add方法是添加单个元素,index参数就是0,这个方法就是在遍历数组,如果数组中已经存在相同元素则返回数组下标,注意看它的比较依据是equals方法;如果不存在则返回-1。

在addIfAbsent所返回的三元表达式中,如果indexOf方法返回数组下标,则返回false,表示已经存在相同元素,添加失败;否则返回-1执行addIfAbsent(e, snapshot),进入该方法:

private boolean addIfAbsent(E e, Object[] snapshot) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] current = getArray();
        int len = current.length;
        if (snapshot != current) {
            // Optimize for lost race to another addXXX operation
            int common = Math.min(snapshot.length, len);
            for (int i = 0; i < common; i++)
                if (current[i] != snapshot[i] && eq(e, current[i]))
                    return false;
            if (indexOf(e, current, common, len) >= 0)
                return false;
        }
        Object[] newElements = Arrays.copyOf(current, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

我们可以忽略if语句,重点关注它的添加操作,发现它也将原数组Copy到长度加一的新数组中,再对新数组进行操作,这个写操作上了锁。其他的写方法都调用了CopyOnWriteArrayList的方法,同样是写操作上锁,读操作可以同时执行。

9. Queue接口(队列)

  Collection的子接口,表示队列FIFO(First In First Out),先进先出。
常用方法:
1.抛出异常:

boolean add(E e)//顺序添加一个元素(到达上限后,再添加则会抛出异常)。
E remove()//获得第一个元素并移除(如果队列没有元素时,则抛出异常)。
E element()//获得第一个元素但不移除(如果队列没有元素时,则抛异常)。

2.返回特殊值:(建议使用)

boolean offer(E e)//顺序添加一个元素(到达上限后,再添加则会返回false)。
E poll()//获得第一个元素并移除(如果队列没有元素时,则返回null)。
E peek()//获得第一个元素但不移除(如果队列没有元素时,则返回null)。
//Queue实现类的使用
public class Demo {
    public static void main(String[] args) {
        //创建队列
        Queue queue=new linkedList();
        //入队
        queue.offer("苹果");
        queue.offer("橘子");
        queue.offer("香蕉");
        queue.offer("西瓜");
        queue.offer("葡萄");
        System.out.println("队首元素:"+queue.peek());
        System.out.println("元素个数:"+queue.size());
        //出队
        int size=queue.size();
        for(int i=0;i 

因为linkedList是线程不安全的集合,所以不能在多线程的环境中使用。

9.1 ConcurrentlinkedQueue类
  • Queue接口的实现类。线程安全、可高效读写的队列,高并发下性能最好的队列。
  • 无锁、CAS(Compare and Swap)比较交换算法,修改的方法包含三个核心参数(V,E,N)。
  • V:要更新的变量;E:预期值;N:新值。只有当V==E,V=N;否则表示V已被更新过,则取消当前操作。

&esmp;&esmp;也就是说假如当前值V是80,要将其改成100,先将V读取出来,读取的V就是预期值;如果预期值E和V相等,就把V的值更新成新值100;如果不等,说明中间有其他线程更新了V,就取消当前操作。

//演示线程安全的队列
public class TestConcurrentlinkedQueue {
    public static void main(String[] args) throws InterruptedException {
        //创建安全队列
        ConcurrentlinkedQueue queue=new ConcurrentlinkedQueue();
        //两个线程执行入队操作
        Thread t1=new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i=1;i<=5;i++) {
                    queue.offer(i);
                }
            }
        });
        Thread t2=new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i=6;i<=10;i++) {
                    queue.offer(i);
                }
            }
        });
        //启动线程
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        for(int i=1;i<=10;i++) {
            System.out.println(queue.poll());
        }
    }
}

因为是两个线程同时添加,所以结果不是顺序的:

1
2
6
3
7
4
8
5
9
10
9.2 BlockingQueue接口(阻塞队列)

&esmp;&esmp;Queue的子接口,阻塞的队列,增加了两个线程状态为无限期等待的方法。

方法:

void put(E e) //将指定元素插入此队列中,如果没有可用空间,则等待。
E take() //获取并移除此队列头部元素,如果没有可用元素,则等待。

可用于解决生产者、消费者问题。

9.2.1 阻塞队列(实现类)

ArrayBlockingQueue: 数组结构实现,有界队列。
linkedBlockingQueue: 链表结构实现,有界队列。默认上限Integer.MAX_VALUE。

通过一个小程序演示一下所谓的阻塞:

//阻塞队列的使用
public class TestArrayBlockingQueue {
    public static void main(String[] args) throws InterruptedException {
        //创建一个有界队列
        ArrayBlockingQueue arrayBlockingQueue=new ArrayBlockingQueue(3);
        //添加数据使用put
        arrayBlockingQueue.put(1);
        arrayBlockingQueue.put(2);
        arrayBlockingQueue.put(3);
        System.out.println(arrayBlockingQueue.size());
        System.out.println(arrayBlockingQueue.toString());
        arrayBlockingQueue.put(4);
        System.out.println("我不会被执行。");
    }
}

该程序执行后可以通过控制台看见程序并没有结束,也没有打印最后一句话,说明当前线程(主线程)被阻塞了:

3
[1, 2, 3]
9.2.2 重写生产者消费者问题
public class ProductConsume {
    public static void main(String[] args) {
        //1.创建队列
        ArrayBlockingQueue queue=new ArrayBlockingQueue(6);
        //2.创建两个线程
        Thread t1=new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i=1;i<=30;i++) {
                    try {
                        queue.put(i);
                        System.out.println("生产者生产了一个产品,产品ID:"+i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        Thread t2=new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i=1;i<=30;i++) {
                    try {
                        queue.take();
                        System.out.println("消费者消费了一个产品,产品ID:"+i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        t1.start();
        t2.start();
    }
}

插入队尾的方法是put,删除队首元素的方法是take。

10. ConcurrentHashMap
  • 初始容量默认为16段(Segment),使用分段锁设计。每一段都对应着一个哈希表。
  • 不对整个Map加锁,而是为每个Segment加锁。对一个Segment的操作不影响其他Segment。
  • 当多个对象存入同一个Segment时,才需要互斥。
  • 最理想状态为16个对象分别存入16个Segment,并行数量16。
  • 使用方式与HashMap无异。

  注:在JDK1.8之后,ConcurrentHashMap不再采用分段锁,而是采用无锁算法CAS。

public class TestConcurrentHashMap {
    public static void main(String[] args) {
        //创建集合
        ConcurrentHashMap hashMap=new ConcurrentHashMap();
        //使用多线程添加数据
        for(int i=0;i<5;i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int k=0;k<10;k++) {
                        hashMap.put(Thread.currentThread().getName(), k);
                        System.out.println(hashMap);
                    }
                }
            }).start();
        }
    }
}
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/582114.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号