Java 高级:多线程

目录

程序、进程、线程的基本概念

线程的创建和使用

Thread类

继承方式和实现方式的联系与区别

Thread类的有关方法

线程的调度

线程的同步

线程安全的单例模式之懒汉式

死锁问题

Lock

线程的通信

JDK5.0新增创建线程方式

实现Callable接口

使用线程池


程序、进程、线程的基本概念

用简单的话解释:

程序就是完成特定任务的一段静态代码

进程就是程序的一次执行过程,或者说正在运行的一个程序

线程是进程的细化,是程序内部执行的一条路径

就像你打开了360安全卫士,360是程序,打开了以后电脑上就有了360的进程,360的各项功能比如查杀木马、清理电脑。开启查杀木马这样的事情就可以理解为开启了一个线程去执行了查杀木马这样的任务。

线程的创建和使用

Java的多线程通过 java.lang.Thread类来体现。

Thread的特性:

  • 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常 把run()方法的主体称为线程体
  • 通过该Thread对象的start()方法来启动这个线程,而非直接调用run()

Thread类

Thread类的构造器:

  • Thread():创建新的Thread对象
  • Thread(String threadname):创建线程并指定线程实例名
  • Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接 口中的run方法
  • Thread(Runnable target, String name):创建新的Thread对

创建线程的两种方式:

  • 继承Thread类
    •  定义子类继承Thread类
    • 子类中重写Thread类中的run方法
    • 创建Thread子类对象,即创建了线程对象
    • 调用线程对象start方法:启动线程,调用run方法
  • 实现Runable接口
    • 定义子类,实现Runnable接口
    • 子类中重写Runnable接口中的run方法
    •  通过Thread类含参构造器创建线程对象
    • 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中
    • 调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法

实例(一):通过继承Thread类创建线程

/**
 * @author Claw
 * @date 2020/6/2 14:20.
 */
// 1.继承Thread类
public class ThreadTest extends Thread {
    /**
     * 2 .重写Run方法
     */
    @Override
    public void run() {
        // run方法里写线程要做的事情
        // 遍历100之内的数,并打印
        for (int i = 0; i < 100; i++) {
            System.out.println(i);
        }
    }

    public static void main(String[] args) {
        // 3.创建Thread子类的对象
        ThreadTest thread = new ThreadTest();
        // 4.调用线程对象的start方法,启动线程,调用run方法
        thread.start();
    }
}

实例(二):实现Runable接口创建线程

// 1.实现Runnable接口
public class RunnableTest implements Runnable {
    /**
     *  2.重写run()方法
     */
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(+i);
        }
    }
    public static void main(String[] args) {
        RunnableTest runnableTest = new RunnableTest();
        // 3.通过Thread类含参构造器创建线程对象,将实现了Runnable接口的实现类对象放入构造器
        Thread thread = new Thread(runnableTest);
        // 4.调用Thread类的start方法,开启线程
        thread.start();
    }
}

需要注意的是:

  •  如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。
  •  run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。
  •  想要启动多线程,必须调用start方法。
  •  一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上 的异常“IllegalThreadStateException”

继承方式和实现方式的联系与区别

Thread类本身也实现了Runable接口

在实际开发中最好是使用实现Runable接口来创建线程

  • 实现的方式没有类的单继承的局限性
  • 实现的方式更适合来处理多个线程有共享数据的情况

Thread类的有关方法

  • void start(): 启动线程,并执行对象的run()方法
  • run():  线程在被调度时执行的操作
  • String getName():  返回线程的名称
  • void setName(String name):设置该线程名称
  • static Thread currentThread(): 返回当前线程。在Thread子类中就 是this,通常用于主线程和Runnable实现类
  • static  void  yield():线程让步
    • 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
    • 若队列中没有同优先级的线程,忽略此方法
  • join() :当某个程序执行流中调用其他线程的 join() 方法时,调用线程将 被阻塞,直到 join() 方法加入的 join 线程执行完为止
    • 低优先级的线程也可以获得执行
  • static  void  sleep(long millis):(指定时间:毫秒)
    • 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后 重排队。
  • stop(): 强制线程生命期结束,不推荐使用
  • boolean isAlive():返回boolean,判断线程是否还活着

线程的调度

调度策略

  • 时间片,一会儿执行蓝色的线程,一会儿执行黑色的线程
  • 抢占式:高优先级的线程抢占CPU

Java的调度方法:

  • 同优先级线程组成先进先出队列,使用时间片策略
  • 对高优先级,使用优先调度的抢占式策略

线程的优先级等级:

  • MAX_PRIORITY:10
  • MIN _PRIORITY:1
  • NORM_PRIORITY:5

涉及的方法:

  • getPriority() :返回线程优先值
  • setPriority(int newPriority) :改变线程的优先级

线程创建时继承父线程的优先级,低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用

线程的同步

实例(一)通过模拟火车票售卖来理解线程安全

并发的问题的本质就是多个线程操作同一个资源,调用三个线程去售票,但这时未加锁的代码结果确是混乱的,会有重复的售票行为,甚至负票。

public class TicketTest2 implements Runnable {
    /**
     * 共同资源:ticket
     */
    private int ticket = 100;

    /**
     * 主方法:调用线程去售票
     * @param args
     */
    public static void main(String[] args) {
        TicketTest2 ticketTest = new TicketTest2();
        Thread thread1 = new Thread(ticketTest);
        Thread thread2 = new Thread(ticketTest);
        Thread thread3 = new Thread(ticketTest);
        
        thread1.setName("窗口1");
        thread2.setName("窗口2");
        thread3.setName("窗口3");

        thread1.start();
        thread2.start();
        thread3.start();
    }

    @Override
    public void run() {
        while (true) {
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "  票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
        }
    }
}

(待补充)

线程安全的单例模式之懒汉式

死锁问题

死锁:

  • 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃 自己需要的同步资源,就形成了线程的死锁
  • 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于 阻塞状态,无法继续

解决办法:

  • 专门的算法、原则
  • 尽量减少同步资源的定义
  • 尽量避免嵌套同步

实例(一)死锁的演示

public class ThreadTest {

    public static void main(String[] args) {

        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();

    }
}

Lock

从JDK5.0开始,Java提供了更强大的线程同步机制,通过显示定义同步锁来实现同步,同步锁使用Lock对象充当。

java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程堆Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象

ReentranLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显示的加锁,释放锁。

实例(一):使用Lock解决线程同步

/**
 * @author Claw
 * @date 2020/6/3 23:17.
 */
public class LockTest implements Runnable {

    private int ticket = 100;
    // 1.实例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                // 2.调用lock()方法
                lock.lock();
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+":售票:"+ticket);
                    ticket--;
                }
            } finally {
                // 3.解锁
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        LockTest lockTest = new LockTest();

        Thread thread1 = new Thread(lockTest);
        Thread thread2 = new Thread(lockTest);
        Thread thread3 = new Thread(lockTest);
        thread1.setName("窗口1");
        thread2.setName("窗口2");
        thread3.setName("窗口3");
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

 

Synchorinezd与Lock的异同

同:都可以解决线程安全问题

不同:

  • Synchorinezd机制在执行完相应的同步代码以后,自动的释放同步监视器
  • Lock需要手动的启动同步(lock),同时结束同步也需要手动的实现(unlock)

优先使用顺序:

Lock -->同步代码块(已经进入了方法体)---->同步方法(方法体外)

线程的通信

线程的通讯涉及到三个方法:

  • wait():令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当 前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有 权后才能继续执行
  • notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待
  • notifyAll():唤醒正在排队等待资源的所有线程结束等待.

这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报 java.lang.IllegalMonitorStateException异常。

这三个方法的调用者必须是同步代码块或者同步方法中的同步监视器,否则会报 java.lang.IllegalMonitorStateException异常。

这三个方法定义在java.lang.Object类里的,

实例(一):使用两个线程打印 1-100。线程1, 线程2 交替打印。

那么如何交替打印呢?让先打印的线程1先挂起,然后线程2进入run方法,执行打印。

所以实现思路是,让notify()写在前面,线程1进入run方法,打印了数字,然后调用wait()方法挂起,释放了锁,此时线程2进入,调用了notify()唤醒挂起的线程1,然后打印数字,然后接下来去调用wait()方法,进入等待,并释放了锁,然后线程1再次进入,唤醒了线程2,如此循环实现了交替打印。

/**
 * @author Claw
 * @date 2020/6/4 3:52.
 */
public class ThreadConnect implements Runnable {
    private int number = 1;

    public static void main(String[] args) {
        ThreadConnect threadConnect = new ThreadConnect();
        Thread thread1 = new Thread(threadConnect);
        Thread thread2 = new Thread(threadConnect);
        thread1.start();
        thread2.start();
        thread1.setName("线程1");
        thread2.setName("线程2");
    }

    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                // 唤醒被wait()的线程
                notify();
                if (number <= 100) {
                    System.out.println(Thread.currentThread().getName() + "打印了" + number);
                    number++;
                    try {
                        // 使调用wait()的方法进入阻塞状态
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else {
                    break;
                }
            }
        }
    }
}

sleep()和wait()的异同?

相同点:

  • 一旦执行方法,都可以使当前线程进入阻塞状态。

不同点:

  • 两个方法的声明位置不同,在Thread()类中申明sleep(),Object类中声明wait()
  • 调用的要求不同,sleep()可以在任何需要的场景下调用,wait()必须使用同步代码块
  • 关于释放同步监视器,如果两个方法都使用在同步方法和同步代码块中,sleep()不会释放锁,wait()会释放锁

JDK5.0新增创建线程方式

实现Callable接口

与使用Runable相比,Callable功能更强大一些

  • 相比run()方法,可以有返回值
  • 方法可以抛出异常
  • 支持泛型的返回值
  • 需要借助FutureTask类,比如获取返回结果

它需要借助FutureTask类,FutureTask类是Future接口的实现类,而FutureTask同时实现了Runbale接口,它既可以作为Runnable被线程执行,又可以作为future得到Callable的返回值

实例(一)使用Callable创建线程

Callable接口创建多线程比实现Runable接口方式更强大

  • 可以有返回值
  • call()可以抛出异常,被外面的操作捕获,获取异常信息
  • Callable是支持泛型的
/**
 * @author Claw
 * @date 2020/6/4 15:20.
 */
public class CallableTest implements Callable {
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }

    public static void main(String[] args) {
        // 3.创建Callable接口实现类对象
        CallableTest callableTest = new CallableTest();
        // 4.将此Callable接口实现类对象作为FutureTask构造器中,创建FutureTask的对象
        FutureTask futureTask = new FutureTask(callableTest);
        // 5.将FutrueTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并start()
        new Thread(futureTask).start();

        try {
            // get()返回值为FutureTask构造器参数Callable实现类重写的call()的返回值
            Object o = futureTask.get();
            System.out.println("总数为:"+o);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

使用线程池

经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。

所以提前创建多个线程,放入线程池中,使用时直接获取,使用完放回池中,可以避免频繁创建、销毁、实现重复利用,类似生活中的公共交通工具。

这样做的的好处是:

  • 提高响应速度(减少了创建新线程的时间)
  • 降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
  • 便于线程管理
    • corePoolSize:核心池的大小
    • maximumPoolSize:最大线程数
    • keepAliveTime:线程没有任务是最多保持多长时间后会终止

线程池相关API

JDK 5.0起提供了线程池相关API:ExecutorService Executors

 ExecutorService:真正的线程池接口。

常见子类ThreadPoolExecutor

  •  void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行 Runnable
  • <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般用来执行 Callable
  •  void shutdown() :关闭连接池

Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

  •  Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
  •  Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
  •  Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池  Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运 行命令或者定期地执行
/**
 * 创建线程的方式四:使用线程池
 *
 * @author Claw
 * @date 2020/6/4 16:48.
 */

/**
 * 内部类,实现Runnable接口,让线程执行run()方法体的内容
 */
class NumberThread implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
            }
        }
    }
}
public class ThreadPool {

    public static void main(String[] args) {
        // Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
        // newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
        // 1.提供指定线程数量的线程池
        ThreadPoolExecutor executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(10);
        // 2.执行指定线程的操作,需要提供实现Runable接口或Callable接口实现类的对象
        executorService.execute(new NumberThread());
        // 3,关闭连接池
        executorService.shutdown();
    }
}

 

手动创建线程池

线程池最好是通过ThreadPoolExecutor的方式创建,这样的处理方式会更加明确线程池的运行规则,避免资源耗尽的风险。

ThreadPoolExecutor的构造函数有七个参数,分别是:corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler

  • corePoolSize :线程池核心线程大小
    • 线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,最小线程数量即是corePoolSize
  • maximumPoolSize:线程池最大线程数量
    • 一个任务被提交到线程池后,首先会缓存到工作队列中,如果工作队列满了,则会创建一个新线程,然后从工作队列中的取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize来指定。
  • keepAliveTime: 空闲线程存活时间
    • 一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定
  • unit keepAliveTime的计量单位
  • workQueue:新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:
    • ArrayBlockingQueue
      • 基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
    • LinkedBlockingQuene
      • 基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
    • SynchronousQuene
      • 一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
    • PriorityBlockingQueue
      • 具有优先级的无界阻塞队列,优先级通过参数Comparator实现。

  • threadFactory
    • 创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等

  • handler
    • 当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,jdk中提供了4中拒绝策略:
      • CallerRunsPolicy:该策略下,在调用者线程(main线程)中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。
      • AbortPolicy:该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。
      • DiscardPolicy:该策略下,直接丢弃任务,什么都不做,也不会抛出异常
      • DiscardOldestPolicy:该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列

如何去理解这七个参数呢?

以银行办理业务做例子,银行办理业务通过柜台来办理。现在银行大厅有五个柜台,而银行的柜台其实并不是全部开放的,空闲的时候有两个柜台固定开发,其他的柜台是关闭的。当两个柜台都有顾客在办理业务的时候,其他顾客只能在休息区等待。当人越来越多的时候,忙不过来了,顾客等待的时间太长了,就会开放关闭的柜台来让顾客办理业务。当人特别多的时候,顾客休息区也满了,此时需要控场,把想要进入银行大厅的人拦截在大厅门外。

我们把柜台理解为线程,那么固定开发的柜台就是corePoolSize(线程池核心线程大小),maximumPoolSize(线程池最大线程数量)就是全部的柜台数量,workQueue(工作队列)就是休息区也就是阻塞队列,人满为患时禁止进入大厅是handler(拒绝策略)。

当人变少的时候,也不再排队了,为了解决繁忙而开设的3个柜台就关闭了,留下固定的办理业务的柜台,也就是所谓的keepAliveTime

实例(一):手动创建线程池

    public static void main(String[] args) {
        // 手动创建线程池
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                // 核心线程数量
                2,
                // 最大线程数量
                5,
                // 空闲线程存活时间
                3,
                TimeUnit.SECONDS,
                // 阻塞队列
                new ArrayBlockingQueue(3),
                // 线程工厂
                Executors.defaultThreadFactory(),
                //拒绝策略
                new ThreadPoolExecutor.AbortPolicy());
    }

实例(二):让线程执行,理解线程池的参数

核心线程数是2,最大线程是5,工作队列是3。当循环2次的时候,始终只有两条线程在执行。当循环6次的时候(核心线程2,工作队列3,已经满了)的时候,就会开启更多的线程去执行任务,知道循环到第8次的时候,就是它创建线程的最大承载量了(最大线程+工作空间),此时会采用拒绝策略,使用AbortPolicy作为拒绝策略,会抛出RejectedExecutionException的异常。

    public static void main(String[] args) {
        // 手动创建线程池
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                2,
                5,
                3,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        try {
            for (int i = 0; i < 2 ; i++) {
                threadPoolExecutor.execute(()->{
                    System.out.println(Thread.currentThread().getName()+":ok");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            // 关闭线程池
            threadPoolExecutor.shutdown();
        }
    }

线程池池的大线程该如何定义?

根据性能,分为CPU密集型和IO密集型

  • CPU 密集型:CPU是几核就是几,可以保持CPU的效率最高
    • 用代码获取当前电脑CPU最大核数: Runtime.getRuntime().availableProcessors
  • IO 密集型 :判断程序中耗IO的线程,可以根据耗IO的线程设定为该线程的两倍

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值