Java多线程

一、简介

多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。

进程

一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。

二、创建线程的三种方式

注意:线程开启不一定立即执行由CPU!调度执行

继承Thread类

  • 自定义线程类继承Thread类
  • 必须重写 run() 方法,该方法是新线程的入口点。
  • 创建线程对象,调用 start() 方法才能执行。
//方式一:继承Thread类
public class MyThread1 extends Thread{
    @Override
    public void run() {
        //run方法线程体
        for (int j = 0; j < 20; j++) {
            System.out.println("在学习多线程"+j);
        }
    }

    public static void main(String[] args) {
        //main线程,主线程

        //创建线程对象,调用start()方法开启线程
        MyThread1 thread1 = new MyThread1();
        thread1.start();

        for (int i = 0; i < 20; i++) {
            System.out.println("多谢线程真简单"+i);
        }
    }
}

小结

  • 子类继承Thread类具备多线程能力
  •  启动线程:子类对象.start()
  • 不建议使用:避免了OOP单继承局限性

实现Runnable接口

  • 自定义线程类实现Runnable类
  • 实现run()方法,编写线程执行体
  • 创建线程对象,调用start()方法
//第二种方式
public class TestThread1 implements Runnable{
    //重写run()方法
    @Override
    public void run() {
        //线程体
        for (int i = 0; i < 200; i++) {
            System.out.println("run方法的:"+i);
        }
    }

    public static void main(String[] args) {
        //创建Runnable接口的实现类对象
        TestThread1 tThread = new TestThread1();
        //创建线程对象,通过线程对象开启线程,代理
        new Thread(tThread).start();

        for (int i = 0; i < 200; i++) {
            System.out.println("主线程的:"+i);
        }
    }
}

买票案例

//多个线程同时操作同一个对象
public class TestThread02 implements Runnable{

    private int tickNumber = 100;

    @Override
    public void run() {
        while (true){
            if (tickNumber<=0){
                break;
            }
            try {
                Thread.sleep( 200 );
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"买到了第"+tickNumber--+"张票");
        }
    }

    public static void main(String[] args) {
        TestThread02 tick = new TestThread02();

        new Thread(tick,"我").start();
        new Thread(tick,"你").start();
        new Thread(tick,"他").start();
    }
}

 龟兔赛跑案例

//模拟龟兔赛跑
public class Race implements Runnable{

    private static String wineer;

    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            //模拟兔子休息
            if (Thread.currentThread().getName().equals( "兔子" )&&i%30==0){
                try {
                    Thread.sleep( 1 );
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //判断比赛是否结束
            boolean over = gameOver( i );
            if (over){
                break;
            }
            System.out.println(Thread.currentThread().getName()+"--->跑了"+i+"步");
        }
    }
    //判断游戏是否结束
    private boolean gameOver(int steps){
        //判断是否有胜利者
        if (wineer!=null){
            return true;
        }else {
            if (steps>=100){
                wineer = Thread.currentThread().getName();
                System.out.println("胜利者是"+wineer);
                return true;
            }
        }
        return false;
    }

    public static void main(String[] args) {
        Race race = new Race();

        new Thread(race,"兔子").start();
        new Thread(race,"乌龟").start();
    }
}

小结

  • 实现接口Runnable具备多线程的能力
  • 启动线程:传入目标对象+Thread对象.start()
  • 建议使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用 

实现Callable接口

  • 实现Callable接口,需要返回值类型
  • 重写call()方法,需要抛出异常
  • 创建目标对象
  • 创建执行服务 ExecutorService eService = Ececutors.newFixedThreadPool(1);
  • 提交执行 Future result = eService.submit(t1);
  • 获取结果 boolean b1 = result.get()
  • 关闭服务 eService.shutdownNow();
public class TestCallable1 implements Callable {
    @Override
    public Integer call() throws Exception
    {
        int i = 0;
        for(;i<100;i++)
        {
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
        return i;
    }
    public static void main(String[] args)
    {
        TestCallable1 ctt = new TestCallable1();
        FutureTask ft = new FutureTask<>(ctt);
        for(int i = 0;i < 100;i++)
        {
            System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);
            if(i==20)
            {
                new Thread(ft,"有返回值的线程").start();
            }
        }
        try
        {
            System.out.println("子线程的返回值:"+ft.get());
        } catch (InterruptedException e)
        {
            e.printStackTrace();
        } catch (ExecutionException e)
        {
            e.printStackTrace();
        }

    }
}

三、静态代理

有真实对象和代理对象,同时真实对象和代理对象都要实现同一个接口,并且代理对象要代理真实对象。

好处

  • 代理对象可以做很多真实对象做不了的事情
  • 真实对象专注做自己的事

四、线程的状态

停止线程

  • 应该让线程自己停下来
  • 应该使用一个标志性的终止变量来终止线程
  • 如:
            if (tickNumber<=0){
                break;
            }

线程休眠

  • sleep(时间)指定当前线程阻塞的毫秒数
  • sleep存在异常InterruptedException
  • sleep时间达到后线程进入就绪状态
  • sleep可以模拟网络延时,倒计时等等
  • 每一个对象都有一把锁,sleep不会释放锁
            try {
                Thread.sleep( 200 );
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

线程礼让

  • yield 礼让线程,让当前正在执行的线程暂停,但是不阻塞
  • 将小县城从运行状态转为就绪状态
  • 让cpu重新调度,礼让不一定成功,因为它们还会一起争抢资源
//礼让线程
public class TestYield {
    public static void main(String[] args) {
        yield y = new yield();

        new Thread(y,"a").start();
        new Thread(y,"b").start();
    }
}
class yield implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"线程开始");
        Thread.yield();
        System.out.println(Thread.currentThread().getName()+"线程停止执行");
    }
}

线程强制执行

  • Join 合并线程,等待此线程执行完成之后,在执行其他线程,其他线程阻塞
  • 就像平时排队有人插队一样
//Join合并线程
public class TestJoin {
    public static void main(String[] args) throws InterruptedException {
        MyJoin join = new MyJoin();

        new Thread(join,"aa").start();

        for (int i = 0; i < 100; i++) {
            if (i==10){
                new Thread().join();
            }
            System.out.println(Thread.currentThread().getName()+"要插队,,嘿嘿"+i);
        }
    }
}
class MyJoin implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+"老老实实排队"+i);
        }
    }
}

观测线程状态

Thread.State

线程状态:

  • NEW

未启动的线程处于该状态

  • RUNNABLE

在Java虚拟机中执行的线程处于此状态

  • BLOCKED

被阻塞等待监视器锁定的线程处于此状态

  • WAITING

正在等待另一个线程执行特定动作的线程处于该状态

  • TIMED_WAITING

正在等待另一个线程执行动作达到指定等待时间的线程处于该状态

  • TERMINATED

已退出的线程处于该状态

注:一个线程可以在给定时间点处于一个状态。这些状态是不反映任何操作系统线程状态的虚拟机状态!

//观测线程状态
public class TestState {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep( 100 );
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("'==========");
        });
        //观测状态
        Thread.State state = thread.getState();
        System.out.println(state);//NEW

        thread.start();
        state = thread.getState();
        System.out.println(state);//Run

        int i=0;
        while (state != Thread.State.TERMINATED){//线程不终止,就一直输出状态
            if (++i==11){
                break;
            }
            Thread.sleep( 100 );
            System.out.println(thread.getState());
        }
    }
}

注意:死亡的线程就不能再一次启动了!

五、线程优先级

  • 每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。
  • Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。
  • 默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。
  • 具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。
  • getPriority()获取线程优先级
  • setPriority(int xxx)设置线程优先级
  • 先设置优先级,在启动线程
  • 优先级高意味着获取调度的概率低,并不是优先级低就不会被调用了

 六、守护(daemon)线程

  • 线程分为守护线程和用户线程
  • 虚拟机必须确保用户线程运行结束
  • 虚拟机不用等待守护线程执行完毕
  • 比如垃圾回收等待,记录操作日志等等
//测试守护线程
//你父母守护你
public class TestDaemon {
    public static void main(String[] args) {
        Parents parents = new Parents();
        You you = new You();

        Thread thread = new Thread(parents);
        thread.setDaemon( true );

        thread.start();

        new Thread(you).start();
    }
}
//父母
class Parents implements Runnable{
    @Override
    public void run() {
        while (true){
            System.out.println("你的父母时时刻刻都守护着你");
        }
    }
}
//你
class You implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 365; i++) {
            System.out.println("在你父母的守护下一年的第"+i+"开心的成长着");
        }
    }
}

七、线程同步机制

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

存在的问题

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

同步的方法

  • synchronized方法和synchronized块
public synchronized void method(int args){}
  • synchronized方法控制对“对象”的访问,,每一个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象锁才能执行,否则线程会阻塞,方法一旦执行,就独占锁,知道该方法运行结束才释放锁,后面被阻塞的线程才能获得这个锁,继续执行

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

同步块

synchronized(Obj){ }

Obj称之为同步监视器

  • Obj 可以是任何对象,但是推荐使用共享资源作为同步监视器
  • 同步方法中不需要指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class

同步监视器的执行过程

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

八、死锁

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

产生死锁的条件

  1. 互斥条件:一个资源每次只能被一个进程使用
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
  3. 不剥夺条件:进程获得的资源,在未使用完毕之前,不能强行剥夺
  4. 循环等待条件:若干进程之间形成一种首尾相连的循环等待资源关系

避免死锁的方法:破环死锁产生的条件中的任意一个或多个就可以避免发生死锁

//死锁:多个线程互相需要对方占有的资源,就可能形成死锁
public class TestDeadLock {
    public static void main(String[] args) {
        Makeup m1 = new Makeup( "韦韦",0 );
        Makeup m2 = new Makeup( "筱筱",1 );

        m1.start();
        m2.start();
    }
}
//口红
class Lipstick{

}
//镜子
class Mirror{

}
class Makeup extends Thread{
    //需要的资源只有一份,所以用static保证
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();

    int choice;//选择
    String name;///化妆的人
    public Makeup(String name, int choice) {
        this.choice = choice;
        this.name = name;
    }

    @Override
    public void run() {
        //化妆
        try {
            makeup();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    //化妆,相互持有对方的锁,即对方的资源
    private void makeup() throws InterruptedException {
        if (choice==0){
            synchronized (lipstick){//获得口红的锁
                System.out.println(this.name+"获得口红");
                Thread.sleep( 1000 );
            }
            synchronized (mirror){//获得镜子
                System.out.println(this.name+"获得镜子");
            }
        }else {
            synchronized (lipstick){//获得镜子的锁
                System.out.println(this.name+"获得镜子");
                Thread.sleep( 1000 );
            }
            synchronized (mirror){//获得口红
                System.out.println(this.name+"获得口红");
            }
        }
    }
}

九、Lock(锁)

  • 从JDK1.5开始,Java提供的强大线程同步机制----通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
  • java.util.concurrent.locks.ReentrantLock类实现Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较明显的是用ReentrantLock,可以显式加锁、释放锁。
//测试Lock锁
public class TestLock {
    public static void main(String[] args) {
        lock l = new lock();

        new Thread(l).start();
        new Thread(l).start();
        new Thread(l).start();
    }
}
class lock implements Runnable{
    int ticknum = 10;

    //定义Lock锁
    private final ReentrantLock lo = new ReentrantLock();

    @Override
    public void run() {
        while (true){
            try {
                //加锁
                lo.lock();
                if (ticknum>0){
                    try {
                        Thread.sleep( 1000 );
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println( ticknum-- );
                }else {
                    break;
                }
            }finally {
                //解锁
                lo.unlock();
            }
        }
    }
}

Lock 和 synchronized 的对比

  • Lock是显式锁 ,需要手动释放锁,synchronized 是隐式锁,出了作用域自动释放锁
  • Lock只有代码块锁,synchronized有代码块所和方法锁
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
  • 优先使用顺序
    • Lock  > 同步代码(已经进入方法体,分配了相应资源)  > 同步方法(在方法体之外)

 十、生产者消费者问题(线程协作)

        生产者消费者问题,也称有限缓冲问题(Bounded-buffer problem),是一个多线程同步问题的经典案例。生产者生成一定量的数据放到缓冲区中,然后重复此过程;与此同时,消费者也在缓冲区消耗这些数据。生产者和消费者之间必须保持同步,要保证生产者不会在缓冲区满时放入数据,消费者也不会在缓冲区空时消耗数据。不够完善的解决方法容易出现死锁的情况,此时进程都在等待唤醒。

线程通信的方法

  • wait() / notify()方法

  • await() / signal()方法

  • BlockingQueue阻塞队列方法

  • 信号量

  • 管道

利用缓冲区解决-->管程法案例

//管程法案例
public class TestPC {
    public static void main(String[] args) {
        SynContatiner synContatiner = new SynContatiner();

        new Productor( synContatiner ).start();
        new Consumer( synContatiner ).start();
    }
}
//生产者
class Productor extends Thread{
    SynContatiner contatiner;
    public Productor(SynContatiner contatiner){
        this.contatiner = contatiner;
    }
    //生产
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("生产了"+i+"只鸡");
            contatiner.push( new Chicken( i ) );
        }
    }
}
//消费者
class Consumer extends Thread{
    SynContatiner contatiner;
    public Consumer(SynContatiner contatiner){
        this.contatiner = contatiner;
    }
    //消费
    @Override
    public void run() {
        for (int i = 1; i < 100; i++) {
            System.out.println("消费了"+contatiner.pop().id+"只鸡");
        }
    }
}
//产品
class Chicken{
    public int id;

    public Chicken(int id){
        this.id = id;
    }
}
//缓冲区
class SynContatiner{
    //需要一个容器大小
    Chicken[] chickens = new Chicken[10];
    //容器计数器
    int count=0;

    //生产者放入产品
    public synchronized void push(Chicken chicken){
        //如果容器满了,等待消费
        if (count==chickens.length){
            //通知消费者消费
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //产品没满,生产产品
        chickens[count]=chicken;
        count++;

        //通知消费者
        this.notify();
    }

    //消费者消费产品
    public synchronized Chicken pop(){
        //判断是否有产品
        if (count==0){
            //等待生产
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //如果可以消费
        count--;
        Chicken chicken = chickens[count];

        //通知生产者生产
        this.notify();
        return chicken;
    }
}

标志位解决-->信号灯法

//信号灯法
public class TestPC2 {
    public static void main(String[] args) {
        TV tv = new TV();

        new Player(tv).start();
        new Watcher( tv ).start();
    }
}
//生产者--演员
class Player extends Thread{
    TV tv;
    public Player(TV tv){
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (i%2==0){
                this.tv.play( "喜羊羊" );
            }else {
                this.tv.play( "广告" );
            }
        }
    }
}
//消费者--观众
class Watcher extends Thread{
    TV tv;
    public Watcher(TV tv){
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            tv.watch();
        }
    }
}
//产品--节目
class TV{
    //T 演员表演,观众等待
    //F 演员等待,观众观看
    String voice;
    boolean flag = true;
    //表演
    public synchronized void play(String voice){
        if (!flag){
            try {
                this.wait(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("演员表演了:"+voice);
        this.notifyAll();//通知唤醒
        this.voice = voice;
        this.flag = !this.flag;
    }
    //观看
    public synchronized void watch(){
        if (flag){
            try {
                this.wait(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("观看了:"+voice);
            this.notifyAll();
            this.flag=!this.flag;
        }
    }
}

十一、线程池

  • 背景:经常创建和销毁、使用量较大的资源,如并发情况下的线程,对性能影响极大。
  • 思路:提前创建好多个线程,放入线程池中,使用的时候直接获取,使用完毕后放回线程池中。可以避免频繁创建和销毁,实现重复利用。类似于生活中的公交车。
  • 好处:
    • 提高响应速度(减少了创建新线程的时间)
    • 降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
    • 便于线程管理

创建线程池,必须满足的条件

  • corePoolSize(线程池基本大小)必须大于或等于0;
  • maximumPoolSize(线程池最大大小)必须大于或等于1;
  • maximumPoolSize必须大于或等于corePoolSize;
  • keepAliveTime(线程存活保持时间)必须大于或等于0;
  • workQueue(任务队列)不能为空;
  • threadFactory(线程工厂)不能为空,默认为DefaultThreadFactory类
  • handler(线程饱和策略)不能为空,默认策略为ThreadPoolExecutor.AbortPolicy。

使用线程池

  •  JDK1.5起提供线程相关的API:ExecutorService和Executors
  • ExecutorService:真正的线程池接口。常见的子类ThreadPoolExecutor
    • void execute(Runnable command):执行任务或命令,无返回值,一般用来执行Runable
    • Future submit(Callable task):执行任务,有返回值,一般用来执行Callable
    • void shutdown():关闭连接池
  • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
//测试线程池
public class TestPool {
    public static void main(String[] args) {
        //创建服务,创建线程池
        ExecutorService service = Executors.newFixedThreadPool( 10 );

        //执行
        service.execute( new MyThread() );
        service.execute( new MyThread() );
        service.execute( new MyThread() );
        service.execute( new MyThread() );
        service.execute( new MyThread() );

        //关闭连接
        service.shutdown();
    }
}
class MyThread implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

你可能感兴趣的:(java,java,学习)