Java|Java多线程学习总结(全面的万字长篇)


Java多线程学习(全面万字长篇)

  • 前言
  • 线程的创建和使用
    • 线程的创建和启动
    • 创建线程的两种方式
    • Thread类相关方法
    • JDK5.0新增创建线程方式
    • 线程的优先级
  • 线程的生命周期
  • 线程的同步(解决共享资源竞争)
    • 方式一:同步代码块
    • 方式二:同步方法
    • 方式三:使用显示的Lock对象
    • synchronized 与 Lock 的对比
  • 线程的通信
  • 生产者和消费者问题

前言
本篇文章只是对多线程做一个简单较全面了解,并不深入探讨 什么时候需要使用多线程? 1、当程序需要同时执行两个或多个任务时。 2、程序需要实现一些需要等待的任务时,如用户输入、文件读写 操作、网络操作、搜索等。 3、需要一些后台运行的程序时。

线程的创建和使用 线程的创建和启动
  1. Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread
    类来体现。
  2. Thread类的特性
  • 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常
    把run()方法的主体称为线程体
  • 通过该Thread对象的start()方法来启动这个线程,而非直接调用run()创建线程的两种方式
方式一:继承Thread类
  1. 定义子类继承Thread类。
  2. 子类中重写Thread类中的run方法。
  3. 创建Thread子类对象,即创建了线程对象。
  4. 调用线程对象start方法:启动线程,调用run方法。
class Even extends Thread{ @Override public void run(){ for(int i=0; i<10; i++){ if(i%2 != 0) System.out.println(Thread.currentThread().getName()+":"+i); } } }

方式二:实现Runnable接口
  1. 定义子类,实现Runnable接口。
  2. 子类中重写Runnable接口中的run方法。
  3. 通过Thread类含参构造器创建线程对象。
  4. 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
  5. 调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。
class Odd implements Runnable{ @Override public void run() { for(int i=0; i<10; i++){ if(i%2==0) System.out.println(Thread.currentThread().getName()+":"+i); Thread.yield(); //让步 /*try { Thread.sleep(100); //休眠 } catch (InterruptedException e) { e.printStackTrace(); }*/ } } } public class ThreadDome01 { public static void main(String[] args) { Even even = new Even(); even.start(); Odd odd = new Odd(); Thread thread = new Thread(odd); thread.start(); } } /* [补充]:1.在run()执行完之后,线程就死亡了 *2.even.start()已经创建了一个线程,不能再写even.start(),如果要再创建 *线程,就必须再new一个对象 */

输出结果: Thread-1:0 Thread-0:1 Thread-0:3 Thread-1:2 Thread-0:5 Thread-1:4 Thread-0:7 Thread-1:6 Thread-0:9 Thread-1:8

推荐使用实现Runnable的方式:
  1. 避免了单继承的局限性
  2. 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线
    程来处理同一份资源。
Thread类相关方法
  1. void start() : 启动线程,并执行对象的run()方法
  2. run(): 线程在被调度时执行的操作
  3. String getName(): 返回线程的名称
  4. void setName(String name):设置该线程名称
  5. static Thread currentThread(): 返回当前线程。在Thread子类中就
    是this,通常用于主线程和Runnable实现
  6. static void yield():线程让步
    - 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
    - 若队列中没有同优先级的线程,忽略此方法
  7. join() :当某个程序执行流中调用其他线程的 join() 方法时,调用线程将
    被阻塞,直到 join() 方法加入的 join 线程执行完为止
    - 低优先级的线程也可以获得执行
  8. static void sleep(long millis):(指定时间:毫秒)
    令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后
    重排队。
JDK5.0新增创建线程方式 新增方式一:使用线程池
线程池相关API:
  1. 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):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
    java SE5的Java.util.concurrent包中的执行器可以为我们管理Thread对象,从而简化并发编程.Executor无须显示地管理线程的生命周期。

class Ex3RunnerA implements Runnable { public Ex3RunnerA() { System.out.println("Constructing Ex3RunnerA"); } public void run() { for(int i = 0; i < 3; i++) { System.out.println("Hi from Ex3RunnerA"); Thread.yield(); } System.out.println("Ex3RunnerA task complete."); // return; } }class Ex3RunnerB implements Runnable { public Ex3RunnerB() { System.out.println("Constructing Ex3RunnerB"); } public void run() { for(int i = 0; i < 3; i++) { System.out.println("Hi from Ex3RunnerB"); Thread.yield(); } System.out.println("Ex3RunnerB task complete."); //return; } }class Ex3RunnerC implements Runnable { public Ex3RunnerC() { System.out.println("Constructing Ex3RunnerC"); } public void run() { for(int i = 0; i < 3; i++) { System.out.println("Hi from Ex3RunnerC"); Thread.yield(); } System.out.println("Ex3RunnerC task complete."); //return; } } public class Ex3 { public static void main(String[] args) { // ExecutorService对象是通过Executors的静态方法创造的 ExecutorService exec1 = Executors.newCachedThreadPool(); exec1.execute(new Ex3RunnerA()); exec1.execute(new Ex3RunnerB()); exec1.execute(new Ex3RunnerC()); exec1.shutdown(); ExecutorService exec2 = Executors.newFixedThreadPool(3); exec2.execute(new Ex3RunnerA()); exec2.execute(new Ex3RunnerB()); exec2.execute(new Ex3RunnerC()); exec2.shutdown(); ExecutorService exec3 = Executors.newSingleThreadExecutor(); //就像是线程数为1的FixedThreadPool exec3.execute(new Ex3RunnerA()); exec3.execute(new Ex3RunnerB()); exec3.execute(new Ex3RunnerC()); exec3.shutdown(); } }

新增方式二:实现Callable接口
与使用Runnable相比, Callable功能更强大些
  • 相比run()方法,可以有返回值
  • 方法可以抛出异常
  • 支持泛型的返回值
  • 需要借助FutureTask类,比如获取返回结
class TaskWithResult implements Callable { private int id; public TaskWithResult(int id){ this.id = id; } @Override public String call(){ return "result of TaskWithResult "+id; } } public class Ex5 { public static void main(String[] args) { ExecutorService exec = Executors.newCachedThreadPool(); ArrayList> results= new ArrayList<>(); for(int i=0; i<10; i++){ results.add(exec.submit(new TaskWithResult(i))); //submit()方法会产生Future对象 } for(Future fs : results){ try { System.out.println(fs.get()); //返回的结果 }catch (InterruptedException e){ System.out.println(e); return; }catch (ExecutionException e){ System.out.println(e); return; }finally { exec.shutdown(); } } } }

submit()方法会产生Future对象,它用Callable返回的结果的特定类型进行了参数化,可以用isDone()方法来查询Future是否已经完成。当完成任务时,它具有一个结果,可以调用get()来获取该结果。
输出结果: result of TaskWithResult 0 result of TaskWithResult 1 result of TaskWithResult 2 result of TaskWithResult 3 result of TaskWithResult 4 result of TaskWithResult 5 result of TaskWithResult 6 result of TaskWithResult 7 result of TaskWithResult 8 result of TaskWithResult 9

线程的优先级
  1. 线程的优先级等级
    - MAX_PRIORITY:10
    - MIN _PRIORITY:1
    - NORM_PRIORITY:5
  2. 涉及的方法
  • getPriority() :返回线程优先值
  • setPriority(int newPriority) :改变线程的优先级
  1. 说明
  • 线程创建时继承父线程的优先级
  • 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
线程的生命周期 Java|Java多线程学习总结(全面的万字长篇)
文章图片

线程的同步(解决共享资源竞争)
首先举个例子——模拟火车站售票程序,开启三个窗口售票。

class Ticket implements Runnable { private int tick = 100; public void run() { while (true) { if (tick > 0) { try { Thread.sleep(100); //将线程安全问题展现更清楚 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread ().getName() + "售出车票,tick号为:" + tick--); } else break; } } } class TicketDemo { public static void main(String[] args) { Ticket t = new Ticket(); Thread t1 = new Thread(t); Thread t2 = new Thread(t); Thread t3 = new Thread(t); t1.setName("t1窗口"); t2.setName("t2窗口"); t3.setName("t3窗口"); t1.start(); t2.start(); t3.start(); } }

部分输出结果: t2窗口售出车票,tick号为:4 t1窗口售出车票,tick号为:4 t3窗口售出车票,tick号为:3 t1窗口售出车票,tick号为:3 t2窗口售出车票,tick号为:1 t3窗口售出车票,tick号为:2问题:多个窗口售票出了相同的票号。 问题原因:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。

防止线程冲突的方法就是当资源被一个任务使用时,在其上加上锁。都一般采取序列化访问共享资源 的方案。
方式一:同步代码块
方式一:同步代码块 *synchronized(同步监视器){ *//需要同步的代码 *} *说明:1、需要同步的代码即使操作共享数据的代码 *2、共享数据:多个线程共同操作的变量,在本例子中tick *3、同步监视器,俗称:锁。任何一个类的对象,都可以充当索。 *要求:多个线程必须要共用同一把锁。

使用同步代码块的方式来改改上面的火车卖票的程序。
下面是实现Runnable接口的方式
class Ticket implements Runnable { private int tick = 100; Object obj = new Object(); @Override public void run() { while (true) { // synchronized (this){//对于实现同步监视器是当前对象t。一般都写this synchronized (obj) {//同步监视器是obj,任何对象都可以当作索。但必须多个线程共用一把锁 if (tick > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread ().getName() + "售出车票,tick号为:" + tick--); } else break; } //} } } }

需要注意的是同步监视器(也就是锁),同步监视器可以是任何对象,但必须是多个线程共用一把锁。对于实现Runnable接口的方式,上面两个都可以,一般用synchronized (this){},
下面是对于继承的方式
继承的方式和实现接口的方式是有所不同的
由于继承方式需要创建3个Ticcket1对象,所以不能用synchronized (this){},
可以将Ticket1类作为同步监视器,即 synchronized (Ticket1.class){ },也可以用其它对象,但是需要声明为静态static的。
class Ticket1 extends Thread { private static int tick = 100; // private static Object obj = new Object(); @Override public void run() { while (true) { synchronized (Ticket1.class){//和下面的都可以 //synchronized (obj) {//同步监视器是obj, if (tick > 0) { try { Thread.sleep(100); //将线程安全问题展现更清楚 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread ().getName() + "售出车票,tick号为:" + tick--); } else break; } //} } } }class TicketDemo02 { public static void main(String[] args) { Ticket1 t1 = new Ticket1(); Ticket1 t2 = new Ticket1(); Ticket1 t3 = new Ticket1(); t1.setName("t1窗口"); t2.setName("t2窗口"); t3.setName("t3窗口"); t1.start(); t2.start(); t3.start(); } }

总的来说就是同步监视器需要是多个线程共用一个
方式二:同步方法 同步方法就是如果操作共享资源的刚好是一个方法,即可以在方法上加上synchronized 关键字就行。
比如对于实现的方式
public synchronized void method(){}

对于继承的方式 需要加上static。
public static synchronized void show(){}

对于同步方法的总结:
  • 同步方法仍然涉及到同步监视器,只是不需要我们显示的声明
  • 非静态的同步方法,同步监视器是:this
  • 静态的同步方法,同步监视器是:当前类本身。
方式三:使用显示的Lock对象 javaSE5的Java.util.concurrent类库还包含有定义在java.util.concurrent.locks中的显示的互斥机制。Lock对象必须显示地创建、锁定和释放。因此,它与内建的锁形式相比。代码缺少点优雅,但是对于某些问题更有优势。
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的
工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象
加锁,线程开始访问共享资源之前应先获得Lock对象。
class A{ private ReentrantLock lock = new ReenTrantLock(); //private Lock lock = new ReenTrantLock(); //也可以 public void m(){ lock.lock(); try{ //保证线程安全的代码; } finally{ lock.unlock(); } } } //注意:如果同步代码有异常,要将unlock()写入finally语句块

注意 如果方法有返回值,return必须在try子句中出现,以确保unlock()不会过早发生。
synchronized 与 Lock 的对比
  1. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是
    隐式锁,出了作用域自动释放
  2. Lock只有代码块锁,synchronized有代码块锁和方法锁
  3. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有
    更好的扩展性(提供更多的子类)
    优先使用顺序:
    Lock ——> 同步代码块(已经进入了方法体,分配了相应资源) ——> 同步方法
    (在方法体之外)
线程的通信
还是以一个例子来讲解:使用两个线程打印 1-100。线程1, 线程2 交替打印

class Communication implements Runnable { int i = 1; public void run() { while (true) { synchronized (this) { notify(); //因为省略了this,相当于this.notify(),必须是同步监视器调用。 if (i <= 100) { System.out.println(Thread.currentThread().getName() + ":" + i++); } else break; try { wait(); //省略了this,相当于this.wait(),必须是同步监视器调用。 } catch (InterruptedException e) { e.printStackTrace(); } } } } } public class CommunicationTest { public static void main(String[] args) { Communication c = new Communication(); Thread t1 = new Thread(c); Thread t2 = new Thread(c); t1.setName("线程1"); t2.setName("线程2"); t1.start(); t2.start(); } }

涉及到的3个方法:
  • wait():一旦执行此方法,当前线程就会进入阻塞状态,并释放同步监视器。
  • notify(): 一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程被wait(),就唤醒优先级较高的。
  • notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
说明
这三个方法必须使用在同步代码块或同步方法中。
这三个方法的调用者必须是同步代码块或同步方法中的同步监视器
否则都会报java.lang.IllegalMonitorStateException异常。
如果同步监视器是其它对象:
private Object obj = new Object(); //省略代码... synchronized (obj) {//obj作为同步监视器 obj.notify(); //省略代码... obj.wait(); //.... }

正因为如此,这三个方法声明在Object类中。
再来说一说wait()和sleep()的区别:
  1. 两个方法声明位置不同。Thread类中声明sleep(),Object类中声明wait();
  2. sleep()可以在任何需要的场景使用,而wait()必须使用在同步代码块或同步方法中
  3. 如果两个方法都是用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放同步监视器。
生产者和消费者问题 通过上面的学习,可以敲一下多线程的经典问题来学以致用。
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品, 店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品, 店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产; 如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。

代码:
class Clerk { // 售货员 private int product = 0; public synchronized void addProduct() { if (product >= 20) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } else { product++; System.out.println("生产者生产了 第" + product + "个产品"); notifyAll(); } } public synchronized void getProduct() { if (this.product <= 0) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } else { System.out.println("消费者取走了第" + product + "个产品"); product--; notifyAll(); } } } class Productor implements Runnable { // 生产者 Clerk clerk; public Productor(Clerk clerk) { this.clerk = clerk; } public void run() { System.out.println("生产者开始生产产品"); while (true) { try { Thread.sleep((int) Math.random() * 1000); } catch (InterruptedException e) { e.printStackTrace(); } clerk.addProduct(); } } } class Consumer implements Runnable { // 消费者 Clerk clerk; public Consumer(Clerk clerk) { this.clerk = clerk; } public void run() { System.out.println("消费者开始取走产品"); while (true) { try { Thread.sleep((int) Math.random() * 1000); } catch (InterruptedException e) { e.printStackTrace(); } clerk.getProduct(); } } }public class ProductTest { public static void main(String[] args) { Clerk clerk = new Clerk(); Thread productorThread = new Thread(new Productor(clerk)); Thread consumerThread = new Thread(new Consumer(clerk)); productorThread.start(); consumerThread.start(); }}

资料:宋红康java多线程讲解
【Java|Java多线程学习总结(全面的万字长篇)】如果读者大大们感觉写的可以,希望给个小小的点赞,你们的鼓励就是我前进的动力。

    推荐阅读