1.相关概念
- 步伐(program):为完成特定任务,用某种语言编写的一组指令的聚集。即指一段静态的代码,静态对象
- 进程(process):步伐的一次执行过程,或是正在内存中运行的应用步伐。如:运行中的QQ,运行中的网易音乐播放器。
- 线程(thread):进程可进一步细化为线程,是步伐内部的一条执行路径。一个进程中至少有一个线程。
- 进程同一时间若并行执行多个线程,就是支持多线程的。
2.创建和启动线程
2.1 方式1:继承Thread类
Java通过继承Thread类来创建并启动多线程的步骤如下:
- 定义Thread 类的子类,并重写该类的 run() 方法,该 run() 方法的方法体就代表了线程必要完成的任务
- 创建 Thread 子类的实例,即创建了线程对象
- 调用线程对象的 start() 方法来启动该线程
- 代码如下:
- package Thread;
- public class ThreadByThread extends Thread {
- @Override
- public void run() {
- for (int i = 1; i <= 100; i++) {
- if (i % 2 == 0) {
- System.out.println(Thread.currentThread().getName() + ":" + i);
- }
- }
- }
- }
- 测试类:
- package Thread;
- public class ThreadTestByThread {
- public static void main(String[] args) {
- ThreadByThread t1 = new ThreadByThread();
- ThreadByThread t2 = new ThreadByThread();
- t1.start();
- t2.start();
- }
- }
复制代码 结果:发现卖出近100张票。
问题:但是有重复票或负数票问题。
原因:线程安全问题
解决方法:同步机制
要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在
票问题,Java中提供了同步机制 (synchronized)来解决。
根据案例简述:
窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结
束,窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改
共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢
夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现
象。
为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。注意:在任
何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能
在外等着 (BLOCKED) 。
同步机制的原理,其实就相称于给某段代码加“锁”,任何线程想要执行这
段代码,都要先得到“锁”,我们称它为同步锁。因为Java对象在堆中的数
据分为分为对象头、实例变量、空白的填充。而对象头中包含:
- Mark Word:记载了和当前对象有关的GC、锁标记等信息。
- 指向类的指针:每一个对象必要记载它是由哪个类创建出来的。
- 数组长度(只有数组对象才有)
哪个线程得到了“同步锁”对象之后,”同步锁“对象就会记载这个线程的
ID,这样其他线程就只能等待了,除非这个线程”开释“了锁对象,其他
线程才能重新得到/占"同步锁"对象。
同步代码块:synchronized 关键字可以用于某个区块前面,表示只对这个区块
的资源实行互斥访问。 格式:- 代码如下:
- package Thread;
- public class ThreadByRunnable implements Runnable{
- @Override
- public void run() {
- for (int i = 1; i <= 100; i++) {
- if (i % 2 == 0) {
- System.out.println(Thread.currentThread().getName() + ":" + i);
- }
- }
- }
- }
- 测试类:
- package Thread;
- public class ThreadTestByRunnable {
- public static void main(String[] args) {
- ThreadByRunnable r = new ThreadByRunnable();
- Thread t1 = new Thread(r);
- Thread t2 = new Thread(r);
- t1.start();
- t2.start();
- }
- }
复制代码
同步方法:synchronized 关键字直接修饰方法,表示同一时刻只有一个线程能
进入这个方法,其他线程在外面等着。- new Thread("新的线程!"){
- @Override
- public void run() {
- for (int i = 0; i < 10; i++) {
- System.out.println(getName()+":正在执行!"+i);
- }
- }
- }.start();
复制代码
静态方法加锁代码:- new Thread(new Runnable(){
- @Override
- public void run() {
- for (int i = 0; i < 10; i++) {
- System.out.println(Thread.currentThread().getName()+":" + i);
- }
- }
- }).start();
复制代码
非静态方法加锁:- package unsafe;
- class TicketSaleThread extends Thread {
- private static int ticket = 100;
- public void run() {
- while (ticket > 0) {
- try {
- Thread.sleep(10);//加入这个,使得问题暴露的更明显
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(getName() + "卖出一张票,票号:" + ticket);
- ticket--;
- }
- }
- }
- public class SaleTicket {
- public static void main(String[] args) {
- TicketSaleThread t1 = new TicketSaleThread();
- TicketSaleThread t2 = new TicketSaleThread();
- TicketSaleThread t3 = new TicketSaleThread();
- t1.setName("窗口1");
- t2.setName("窗口2");
- t3.setName("窗口3");
- t1.start();
- t2.start();
- t3.start();
- }
- }
复制代码
6. 再谈同步
6.1 死锁
差别的线程分别占用对方必要的同步资源不放弃,都在等待对方放弃自己必要
的同步资源,就形成了线程的死锁。
诱发死锁的原因:
- 互斥条件
- 占用且等待
- 不可抢夺(或不可抢占)
- 循环等待
以上4个条件,同时出现就会触发死锁。
解决死锁:
死锁一旦出现,基本很难人为干预,只能只管规避。可以思量打破上面的诱发条件。
针对条件1:互斥条件基本上无法被破坏。因为线程必要通过互斥解决安全问题。
针对条件2:可以思量一次性申请全部所需的资源,这样就不存在等待的问题。
针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就自动开释掉已经占用的资源。
针对条件4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。
6.2 JDK5.0 新特性:LOCK(锁)
- JDK5.0的新增功能,保证线程的安全。与采用synchronized相比,Lock可
提供多种锁方案,更灵活、更强大。Lock通过显式定义同步锁对象来实现同
步。同步锁使用Lock对象充当。
- java. util. concurrent. locks. Lock 接口是控制多个线程对共享资源进行访
问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对
象加锁,线程开始访问共享资源之前应先得到Lock对象。
- 实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、开释锁。
- ReentrantLock类实现了 Lock 接口,它拥有与 synchronized 类似的
并发性和内存语义,但是添加了类似锁投票、定时锁等候和可停止锁等
候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。
- Lock锁也称同步锁,加锁与开释锁方法,如下:
- public void lock() :加同步锁。
- public void unlock() :开释同步锁。
代码如下:- package safe;
- class TicketSaleRunnable implements Runnable {
- private int ticket = 100;
- public void run() {
- while (ticket > 0) {
- try {
- Thread.sleep(10);//加入这个,使得问题暴露的更明显
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(Thread.currentThread().getName() + "卖出一张票,票号:" + ticket);
- ticket--;
- }
- }
- }
- public class SaleTicket {
- public static void main(String[] args) {
- TicketSaleRunnable tr = new TicketSaleRunnable();
- Thread t1 = new Thread(tr, "窗口一");
- Thread t2 = new Thread(tr, "窗口二");
- Thread t3 = new Thread(tr, "窗口三");
- t1.start();
- t2.start();
- t3.start();
- }
- }
复制代码
synchronized 与 Lock 的对比
- Lock 是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域、遇到异常等自动解锁
- Lock 只有代码块锁,synchronized 有代码块锁和方法锁
- 使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类),更表现面向对象。
- (了解)Lock 锁可以对读不加锁,对写加锁,synchronized 不可以
- (了解)Lock 锁可以有多种获取锁的方式,可以从 sleep 的线程中抢到锁,synchronized 不可以
说明:开辟建议中处理线程安全问题优先使用顺序为:
Lock ----> 同步代码块 ----> 同步方法
7. 线程的通讯
生产者与消耗者问题
等待唤醒机制可以解决经典的“生产者与消耗者”的问题。生产者与消耗者问
题,也称有限缓冲问题,是一个多线程同步问题的经典案例。该问题描述了两个
(多个)共享固定巨细缓冲区的线程——即所谓的“生产者”和“消耗者”——
在实际运行时会发生的问题。
生产者的主要作用是天生肯定量的数据放到缓冲区中,然后重复此过程。与此
同时,消耗者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在
缓冲区满时参加数据,消耗者也不会在缓冲区中空时消耗数据。
举例:
生产者(Productor)将产品交给伙计(Clerk),而消耗者(Customer)从伙计
处取走产品,伙计一次只能持有固定数量的产品(比如:20),如果生产者试
图生产更多的产品,伙计会叫生产者停一下,如果店中有空位放产品了再通
知生产者继续生产;如果店中没有产品了,伙计会告诉消耗者等一下,如果
店中有产品了再通知消耗者来取走产品。
类似的场景,比如厨师和服务员等。
生产者与消耗者问题中其实隐含了两个问题:
- 线程安全问题:
因为生产者与消耗者共享数据缓冲区,产生安全问题。不过这个问题可以使
用同步解决。
- 线程的和谐工作问题:
要解决该问题,就必须让生产者线程在缓冲区满时等待(wait),暂停进入阻
塞状态,等到下次消耗者消耗了缓冲区中的数据的时候,通知(notify)正在
等待的线程恢复到就绪状态,重新开始往缓冲区添加数据。同样,也可以让
消耗者线程在缓冲区空时进入等待(wait),暂停进入阻塞状态,等到生产者
往缓冲区添加数据之后,再通知(notify)正在等待的线程恢复到就绪状态。
通过这样的通讯机制来解决此类问题。
代码实现:
- synchronized(同步锁){
- 需要同步操作的代码
- }
复制代码
8. JDK5.0 新增线程创建方式
8.1 新增方式一:实现 Callable接口
- 与使用Runnable相比, Callable功能更强大些
– 相比run()方法,可以有返回值
– 方法可以抛出异常
– 支持泛型的返回值(必要借助FutureTask类,获取返回结果)
– 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
– FutureTask是Futrue接口的唯一的实现类
– FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
- 缺点:在获取分线程执行结果的时候,当前线程(或是主线程)受阻塞,效率较低。
代码举例:
[code]/* * 创建多线程的方式三:实现Callable (jdk5.0新增的) *///1.创建一个实现Callable的实现类class NumThread implements Callable { //2.实现call方法,将此线程必要执行的操作声明在call()中 @Override public Object call() throws Exception { int sum = 0; for (int i = 1; i |