一、目前存在的问题
public class MyTest1 { public static void main(String[] args) { for(int i = 0; i < 100; i++) { System.out.println("吃饭"); System.out.println("喝酒"); System.out.println("聊天"); } } }
程序同一时刻只能做一件事情。
#二、基本概念
#2.1、程序、进程、线程
程序(program):是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
进程(process):是程序的一次执行过程,或是正在运行的一个程序。动态过程:有它自身的产生、存在和消亡的过程。
运行中的QQ,运行中的MP3播放器;
程序是静态的,进程是动态的;
进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
线程(thread):进程可进一步细化为线程,是一个程序内部的一条执行路径。
- 若一个进程同一时间并行执行多个线程,就是支持多线程的;
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小;
- 一个进程中的多个线程共享相同的内存单元/内存地址空间,它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患;
- 每个Java程序都有一个隐含的主线程:main方法。
#2.2、多核CPU
单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。
如果是多核的话,才能更好的发挥多线程的效率,现在的服务器都是多核的。
#2.3、多线程应用场合
- 程序需要同时执行两个或多个任务;
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等;
- 需要一些后台运行的程序时。
#三、线程的创建和使用
Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread类来体现。
Thread类的特性:
- 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体;
- 通过该Thread对象的start()方法来启动这个线程,而非直接调用run()。
构造器:
- Thread():创建新的Thread对象;
- Thread(String threadname):创建线程并指定线程实例名;
- Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接口中的run方法;
- Thread(Runnable target, String name):创建新的Thread对象。
JDK5之前创建线程有两种方式:
- 继承Thread类的方式;
- 实现Runnable接口的方式。
#3.1、继承Thread类的
步骤:
- 定义子类继承Thread类;
- 子类重写Thread类中的run方法;
- 创建Thread子类对象,即创建线程对象;
- 调用线程对象start方法(启动线程,调用run方法)。
public class ChatThread extends Thread{ @Override public void run() { for(int i = 0; i < 10; i++) { System.out.println("聊天"); try { Thread.currentThread().sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } public class DrinkThread extends Thread { @Override public void run() { for(int i = 0; i < 10; i++) { System.out.println("喝酒"); try { Thread.currentThread().sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } public class EatThread extends Thread{ @Override public void run() { for(int i = 0; i < 10; i++) { System.out.println("吃饭"); try { Thread.currentThread().sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } public class MyTest2 { public static void main(String[] args) { //创建线程 ChatThread chat = new ChatThread(); DrinkThread drink = new DrinkThread(); EatThread drink = new EatThread(); //启动线程 chat.start(); drink.start(); drink.start(); } }
#3.2、实现Runnable接口
步骤:
- 定义子类实现Runnable接口;
- 子类中重写Runnable接口中的run方法;
- 通过Thread类构造方法创建线程对象;
- 将Runnable接口的子类对象作为实际参数传递给Thread类的构造方法中;
- 调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。
public class ChatThread implements Runnable { @Override public void run() { for(int i = 0; i < 100; i++) { System.out.println("聊天"); try { Thread.currentThread().sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } public class DrinkThread implements Runnable { @Override public void run() { for(int i = 0; i < 100; i++) { System.out.println("喝酒"); try { Thread.currentThread().sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } public class EatThread implements Runnable { @Override public void run() { for(int i = 0; i < 100; i++) { System.out.println("吃饭"); try { Thread.currentThread().sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } public class MyTest2 { public static void main(String[] args) { Thread chat = new Thread(new ChatThread()); Thread drink = new Thread(new DrinkThread()); Thread eat = new Thread(new EatThread()); chat.start(); drink.start(); eat.start(); } }
优势:
- 避免了单继承的局限性;
- 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。
#3.3、Thread类相关方法
相关方法:
- start():启动线程,并执行对象的run()方法;
- run():线程在被调用时执行的操作;
- getName():返回线程的名称;
- setName():设置线程的名称;
- currentThread():返回当前线程。
public class MyThread extends Thread{ //打印100行***** @Override public void run() { for(int i = 0; i < 100; i++) { //返回线程的名称 System.out.println("*****" + Thread.currentThread().getName() + ":" + i); try { //延时100ms Thread.currentThread().sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class MyTest3 { public static void main(String[] args) throws InterruptedException { //设置主线程名称 Thread.currentThread().setName("主线程"); MyThread mythread = new MyThread(); //设置自定义线程名称 mythread.setName("子线程"); //启动自定义线程 mythread.start(); //打印100行%%%%% for (int i = 0; i < 30; i++) { System.out.println("%%%%%" + Thread.currentThread().getName() + ":" + i); try { //延时100ms Thread.currentThread().sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }
#四、线程的调度和生命周期
#4.1、线程的调度
调度策略:
- 基于时间片轮转;
- 抢占式:高优先级的线程抢占CPU。
调度方法:
- 同优先级线程组成先进先出队列,使用时间片策略
- 对高优先级,使用优先调度的抢占式策略
- Java中线程优先级的范围是1~10,默认的优先级是5
- MAX_PRIORITY(10)
- MIN_PRIORITY(1)
- NORM_PRIORITY(5)
涉及的方法:
- setPriority(int newPriority):设置线程的优先级
- getPriority():获取线程的优先级
- yield():线程让步
- 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
- 若队列中没有同优先级的线程,忽略此方法
- join():
- 当某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到join()方法加入的join线程执行完为止
- 低优先级的线程也可以获得执行
- sleep(long millis):令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队
- stop():强制线程生命期结束
- isAlive():判断线程是否还活着
public class MyTest3 { public static void main(String[] args) throws InterruptedException { Thread.currentThread().setName("主线程"); MyThread mythread = new MyThread(); mythread.setName("子线程"); mythread.start(); //设置线程的优先级 mythread.setPriority(Thread.MAX_PRIORITY); //打印100行%%%%% for (int i = 0; i < 30; i++) { System.out.println("%%%%%" + Thread.currentThread().getName() + ":" + i); if(i == 2) { //Thread.currentThread().yield(); //mythread.stop(); mythread.join(); System.out.println(mythread.isAlive()); } try { Thread.currentThread().sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
#4.2、线程的生命周期
新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态;
就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件;
运行:当就绪的线程被调度并获得处理器资源时,便进入运行状态, run()方法定义了线程的操作和功能;
阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态;
死亡:线程完成了它的全部工作或线程被提前强制性地中止。
#五、线程同步
public class Window implements Runnable { private int count = 100; @Override public void run() { while(true) { if(count > 0) { try { Thread.currentThread().sleep(6); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "卖第" + (count--) + "张票"); } else { break; } } } } public class MyTest4 { public static void main(String[] args) { Window window = new Window(); Thread window1 = new Thread(window); Thread window2 = new Thread(window); Thread window3 = new Thread(window); window1.start(); window2.start(); window3.start(); } }
现象:
- 多个窗口卖同一张票;
- 票的编号出现了0或者负值。
问题的原因:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
解决思路:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
Java对于多线程的安全问题提供了专业的解决方式:同步机制。
#5.1、synchronized代码块
public class Window1 implements Runnable { private int count = 100; @Override public void run() { while(true) { //只剩一张票 synchronized (this) { if(count > 0) { System.out.println(Thread.currentThread().getName() + "卖第" + (count--) + "张票"); } else { break; } } try { Thread.currentThread().sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }
#5.2、synchronized方法
//所有窗口共用100张票 public class Window2 implements Runnable { private int count = 100; @Override public void run() { while(true) { sale(); if(count <= 0) { break; } try { Thread.currentThread().sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } //同步方法 该方法运行完毕之后,其他的线程才会运行 public synchronized void sale() { if(count > 0) { System.out.println(Thread.currentThread().getName() + "卖第" + (count--) + "张票"); } } }
#5.3、Lock锁
从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock类实现了Lock ,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
import java.util.concurrent.locks.ReentrantLock; public class Window3 implements Runnable { private int count = 100; private ReentrantLock lock = new ReentrantLock(true); @Override public void run() { while(true) { try { lock.lock(); if(count > 0) { System.out.println(Thread.currentThread().getName() + "卖第" + (count--) + "张票"); } else { break; } } finally { lock.unlock(); } try { Thread.currentThread().sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }
#5.4、线程安全的单例模式懒汉式
//synchronized代码块方式 public class SingleObject { private static SingleObject obj = null; private SingleObject() { } public static SingleObject getInstance() { synchronized(SingleObject.class) { if(obj == null) { obj = new SingleObject(); } } return obj; } } //synchronized方法方式 public class SingleObject { private static SingleObject obj = null; private SingleObject() { } public static synchronized SingleObject getInstance() { if(obj == null) { obj = new SingleObject(); } return obj; } } //Lock方式 public class SingleObject { private static SingleObject obj = null; private ReentrantLock lock = new ReentrantLock(true); private SingleObject() { } public static SingleObject getInstance() { try { lock.lock(); if(obj == null) { obj = new SingleObject(); } } finally { lock.unlock(); } return obj; } }
#5.5、死锁
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
public class Lock { static Object objA = new Object(); static Object objB = new Object(); } public class MyThreadA extends Thread { @Override public void run() { while(true) { synchronized (Lock.objA) { synchronized (Lock.objB) { System.out.println("MyThreadA..."); } } } } } public class MyThreadB extends Thread { @Override public void run() { while(true) { synchronized (Lock.objB) { synchronized (Lock.objA) { System.out.println("MyThreadB..."); } } } } } public class MyTest5 { public static void main(String[] args) { MyThreadA a = new MyThreadA(); MyThreadB b = new MyThreadB(); a.setPriority(Thread.MAX_PRIORITY); b.setPriority(Thread.MIN_PRIORITY); a.start(); b.start(); } }
解决方法:
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
#六、线程通信
相关方法:
- wait():令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行;
- notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待;
- notifyAll():唤醒正在排队等待资源的所有线程结束等待。
以上三个方法只有在synchronized方法或synchronized代码块中才能使用。
两个线程,交替打印1~100。
public class PrintNum implements Runnable { int i = 1; @Override public void run() { while (true) { synchronized (Object.class) { //唤醒正在排队等待同步资源的线程中优先级最高者结束等待 Object.class.notify(); if (i <= 100) { System.out.println(Thread.currentThread().getName() + ":" + i++); } else break; try { //令当前线程挂起并放弃CPU、同步资源并等待 Object.class.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } public class MyTest6 { public static void main(String[] args) { PrintNum printNum = new PrintNum(); Thread th1 = new Thread(printNum); Thread th2 = new Thread(printNum); th1.start(); th2.start(); } }
生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个线程——即所谓的生产者和消费者——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。
import java.util.ArrayList; import java.util.List; //仓库 public class Factory { //最大容量 public static final int MAX_SIZE = 100; private static List
#七、JDK5.0新增线程创建方式
#7.1、实现Callable接口
与使用Runnable相比, Callable功能更强大些
- 相比run()方法,可以有返回值;
- 方法可以抛出异常;
- 支持泛型的返回值;
- 需要借助FutureTask类,比如获取返回结果。
关于Future接口
- 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等;
- FutrueTask是Futrue接口的唯一的实现类;
- FutureTask同时实现了Runnable、Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
import java.util.concurrent.Callable; public class NumThread implements Callable{ //重写call方法 @Override public Integer call() throws Exception { //1+2+3+4+...+100 int sum = 0; for(int i = 1; i <= 100; i++) { sum += i; } return sum; } } public class MyTest8 { public static void main(String[] args) { NumThread numThread = new NumThread(); FutureTask task = new FutureTask<>(numThread); //创建线程 Thread thread = new Thread(task); //启动线程 thread.start(); //设置线程优先级 thread.setPriority(Thread.MAX_PRIORITY); try { Integer sum = task.get(); System.out.println(sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }
#7.2、使用线程池
#7.2.1、存在问题及解决思路
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程, 对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完 放回池中。可以避免频繁创建销毁、实现重复利用。
好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理:
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
#7.2.2、线程池相关API
JDK 5.0起提供了线程池相关API:ExecutorService和Executors
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
- void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
- Future submit(Callable task):执行任务,有返回值,一般又来执行Callable
- void shutdown():关闭连接池
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
- Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
- Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池
- Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
- Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
public class NumberThread implements Runnable { @Override public void run() { for (int i = 1; i <= 100; i++) { if(i % 2 == 0) { System.out.println(Thread.currentThread().getName() + ":" + i); } } } } public class NumberThread1 implements Runnable { @Override public void run() { for (int i = 1; i <= 100; i++) { if(i % 2 != 0) { System.out.println(Thread.currentThread().getName() + ":" + i); } } } } public class MyTest9{ public static void main(String[] args) { //创建线程池 ExecutorService service = Executors.newFixedThreadPool(10); NumberThread numberThread = new NumberThread(); NumberThread1 numberThread1 = new NumberThread1(); //执行线程 service.execute(numberThread); service.execute(numberThread1); //关闭线程 service.shutdown(); } }