复习
-
设计模式:一类问题的通用的解决方案,由众多程序员在开发中总结出来的
-
装饰者模式:
- 为对象动态的添加新的增强功能,不改变对象原有的内容
- 使用装饰者的对象,包裹被装饰者对象,使用时,调用装饰者的相关方法,以提供增强功能
- 实现步骤:
- 声明装饰者类,继承被装饰者类 或 和被装饰者继承相同的类
- 声明一个被装饰者类型的属性,用于关联被装饰者对象
- 声明一个包含被装饰者类型的参数的构造器
- 重写被装饰者所有的方法,默认直接调用被装饰者对应的方法
- 重写需要添加增强功能的方法,并提供必要的属性等其他内容
- 有什么实际应用的案例?
- I/O中的设计: BufferedOutputStream
-
线程
- 线程是进程中运行的独立的代码片段(逻辑流)
- 是CPU调度的最小单位
-
线程和进程的关系
- 进程是计算机中运行的独立的程序,是资源分配的最小单位
- 线程依赖于进程存在
- 一个进程中的多个线程,共享进程被分配到的内存空间
-
Java中创建线程的方式
- 实现Runnable接口,重写run方法,以提供线程启动后执行的逻辑
- 创建Thread对象,添加Runnable接口实现类的对象
- 调用Thread对象的start()方法
-
start()方法和run()方法的区别
- start()方法用于让当前线程做好运行的准备,处于可运行状态
- run()方法是当前线程真正运行时所执行的逻辑
总结
- 线程部分的内容要循序渐进的进行学习,从简单到复杂,一点点来,先重复理解了基础的概念,再去学习更深入的概念
- 先弄懂一个线程执行的逻辑,比如线程的状态,在各状态间是如何切换的,如start(),run(),sleep(),join(),yield()等方法的功能和对线程状态的影响
- 然后再去研究多个线程同步的问题,即多个线程同时操作同一个对象的同一个属性时,可能出现的问题
- 多个线程同时操作方法的局部变量,没有线程安全问题
- 多个线程同时仅对同一个对象的同一个属性进行读取操作,没有线程安全问题
- 多个线程同时对同一个对象的同一个属性进行读或写操作,可能有线程安全问题
- 解决线程安全问题的方案-同步-synchronized
- 线程之间的交互-wait和notify和notifyAll
线程
线程的概念和原理
-
进程 — 计算机中运行的一个程序,是资源分配的最小单位
-
线程 — 进程中独立运行的代码片段,是CPU调度的最小单位
-
目前流行的操作系统,如Windows、Solaris、Linux等均支持多线程同时运行。
-
多线程系统可最大限度地发挥CPU的工作效率。
- 硬盘数据读取速度 约150MB/s
- DDR4内存读取速度 约20GB/s
- L3级缓存读取速度 约180GB/S
- L2级缓存读取速度 约370GB/S
- L1级缓存读取速度 约970GB/s
- CPU每秒可执行的操作次数 3.8*10亿次
- CPU可以在不同的程序(进程)间切换,来运行不同的程序,由于切换速度非常快,给人的感觉就是多个程序在同时运行
- CPU也可以在一个程序(进程)的多个逻辑流(线程)间进行切换,使一个程序的不同功能得以实现,或者为不同用户提供相同的功能。
- 线程切换的开销远低于进程切换的开销
-
Java 支持多线程同时运行,它们由CPU 时间片调度。
-
可以将JVM 看作是一个进程,线程与进程的关系模型如下图所示:
线程的创建、运行
- 在Java程序中,可以编程创建多个线程。
- 基本的线程API位于java.lang包。
- main方法就是在线程中执行的。
- 除了main线程外,应用程序还可以自己创建和管理任意数量的线程。
- 可以使用java.lang.Thread和java.lang. Runnable等线程相关的类和接口来创建线程。
1. 创建和启动线程
-
创建一个线程并启动执行它的步骤如下:
-
编写一个java.lang.Runnable接口的实现子类,并实现该接口的run()方法。方法中的代码便是线程代码片段。
-
以第1步的实例为参数,创建一个java.lang.Thread对象。
-
调用Thread对象的start()方法,启动线程执行。
-
public class HelloRunner implements Runnable {
int i;
public void run() {
for (i = 0; i < 100; i++) {
System.out.print("**" + i);
}
}
}
public class TestThread {
public static void main(String[] args) {
Runnable r = new HelloRunner();
Thread t = new Thread(r); //创建线程
t.start(); //启动线程
for (int i = 0; i < 100; i++) {
System.out.print("--" + i);
}
}
}
-
线程对象被创建后,JVM自动赋予线程一个名称。
-
Thread类提供了静态方法currentThread(),可来获得当前线程所在的Thread对象,对象中封装了JVM赋予该线程的名称。
public class HelloRunner implements Runnable { int i; public void run() { Thread curThread = Thread.currentThread(); String name = curThread.getName(); for (i = 0; i < 100; i++) { System.out.print("[" + name + "]-" + i); } } }
public class TestThread { public static void main(String[] args) { Runnable r = new HelloRunner(); Thread t = new Thread(r); //创建线程 t.start(); //启动线程 Thread curThread = Thread.currentThread(); String name = curThread.getName(); for (int i = 0; i < 100; i++) { System.out.print("[" + name + "]-" + i); } } }
2. 线程的调度
- Java中线程是抢占式的,而不是完全的时间片式的。将抢占式等同于时间片式是常见的误区。
- 完全的时间片式是指为每个线程分配等量的CPU 时间的过程。在抢占式模型中,一个线程可持续运行至结束,或被另一个线程取代执行,这是通过CPU 调度来完成的。
练习
-
在main方法中创建并启动一个线程,该线程随机产生100个0-100整数,打印后结束;
-
在main方法中创建并启动两个线程。第一个线程统计指定目录下的文件个数及所占总空间数并打印输出,另一个线程打印输出此目录中每个文件信息如文件名称和大小。
3. 结束线程 — 使用通知方式
-
当线程完成执行并结束后,就无法再次运行了。
-
应该通过使用标志来指示run方法退出的方式来停止线程,即通知方式。
-
该方式可确保线程以安全的方式结束运行。
public class HelloRunner implements Runnable { private int i; private boolean stopFlag = false; public void run() { while (!stopFlag) { System.out.print((i++) + " "); if (i > 500) i = 0; } } public void setStopFlag(boolean stopFlag) { this.stopFlag = stopFlag; } }
public class TestEndThread { public static void main(String[] args) { HelloRunner r = new HelloRunner(); Thread t = new Thread(r); t.start(); for (int i = 0; i < 100; i++) { System.out.print("--" + i); } r.setStopFlag(true); } }
练习
- 在main方法中创建并启动两个线程。第一个线程循环随机打印100以内的整数,直到第二个线程从键盘读取了“Q”命令。
创建线程的另一种方法
-
使用实现Runnable接口的类来创建线程类是标准的创建方式,但这并不是创建线程的唯一方式。
-
Thread 类本身已实现了Runnable接口,所以可通过创建Thread子类来创建线程类。
public class AnotherThread extends Thread { int i; public void run() { for (i = 0; i < 100; i++) { System.out.print("**" + i); } } public static void main(String[] args) { Thread t = new AnotherThread (); t.start(); } }
-
实现Runnable的优点:
- 更好的面向对象设计
- 单继承:这种方式不占用继承窗口
- 一致性
-
继承Thread 的优点
- 代码会更加简洁。
-
通常情况下,建议使用第一种方式。
对线程的控制
1. 使线程睡眠 - sleep方法
-
一个线程在运行过程中,可以调用Thread.sleep()方法以暂停(睡眠)一个指定时间。可能的原因有:
- 该线程应该给其它线程运行的机会
- 该线程可能需要等待对某个资源的访问,直到资源可用时再继续执行。
-
sleep方法是一个静态方法,可使调用该方法的线程暂停一段时间(由参数指定),这也称为睡眠。
-
睡眠中的线程有两种唤醒方式:
- 主动唤醒:当到达指定睡眠时间后
- 被动唤醒:被另一个线程调用了interrupt方法,因此而收到InterruptedException唤醒信号。
public class Printer implements Runnable { public void run() { try { Thread.sleep(10000); } catch (InterruptedException e) { //e.printStackTrace(); //该异常属于唤醒信号,不必处理 } for (int i = 0; i < 100; i++) { System.out.print(i + " "); try { Thread.sleep(100); } catch (InterruptedException e) { } } System.out.println(); } }
public class TestThreadSleep { public static void main(String[] args) { Printer print = new Printer(); Thread t = new Thread(print); t.start(); t.interrupt(); } }
2. 线程的阻塞状态
- 当线程调用了Thread.sleep()方法后,进入阻塞状态。
- 当被阻塞线程醒来后,将回到可运行状态。当然这并不意味着其立即执行,而是需要等待被调度到运行状态后才能继续执行。
练习
- 编写程序,在main方法中创建一个线程。线程每隔一定时间(200ms以内的随机时间)产生一个0-100之间的随机整数,打印后将该整数放到集合中;
- 共产生100个整数,全部产生后,睡眠30秒,然后将集合内容打印输出;
- 在main线程中,唤醒上述睡眠的线程,使其尽快打印集合内容。
3. 使线程睡眠 — join方法
-
join方法会使当前线程进入阻塞状态,直到其所等待的线程结束,或到达指定的时间。
-
与sleep方法不同,join方法是一个实例方法。
-
因调用join方法而阻塞的线程有三种唤醒方式:
- 线程结束唤醒:当所join的线程运行结束后
- 主动唤醒:当到达指定阻塞时间后
- 被动唤醒:被另一个线程调用了interrupt方法,因此而收到InterruptedException唤醒信号。
-
以上3种情况,不管哪种先发生,线程都会醒来
public class Generator implements Runnable { private int counter = 100; public void run() { while (counter != 0) { System.out.print(counter + " "); counter -= 1; } System.out.println(); } }
```java
public class TestThreadJoin {
public static void main(String[] args) {
Generator g = new Generator();
Thread t1 = new Thread(g);
t1.start();
try {
t1.join();
} catch (InterruptedException e) {}
System.out.println("---------------");
}
}
练习
- 改进上一个练习,用以下方式一实现:在main方法中等待第一个线程工作完成后,计算集合中所有整数的和并打印输出;
- main不再唤醒子线程,而是等待子线程运行完毕后,执行本练习的要求,为了提高效率,子线程生成数字后,睡眠2S后就输出
- 改进上一个练习,用以下方式二实现:在main方法中创建第二个线程。该线程等待第一个线程工作完成后,计算集合中所有整数的和并打印输出。
线程同步与交互
线程共享数据
1. 线程共享数据的安全
-
编写一个线程程序(Counter类),声明实例变量counter,初值为200。在run方法中循环50次,每次对counter做减2操作(用-=实现),睡眠10毫秒,并打印counter值,然后继续下一次循环;
-
在main方法中创建Counter实例,并用它创建和运行一个线程,观察counter值每次是否减去了2;
-
用第2项中的Counter实例再创建第二个线程,两个线程同时运行,观察counter值每次是否减去了2 。
public class Counter implements Runnable {
private int counter = 200;
public void run() {
for (int i = 0; i < 50; i++) {
counter -= 2;
try { Thread.sleep(10); }catch(Exception e){}
System.out.print(counter + " ");
}
}
}
public class TestThreadSyncProblem {
public static void main(String[] args) {
Counter c = new Counter();
Thread t1 = new Thread(c);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}
- 由于多线程之间存在相互抢占(CPU的执行权),抢占发生在代码每一步,导致多线程数据并发安全问题(重复、负数、跳过等等错误数据)
- 多线程中方法的局部变量不会存在线程安全问题
- 多线程中,多个线程操作同一个对象的同一个属性,可能存在线程安全问题
线程的同步
-
同步语句块机制可以保证线程中那些不可分割执行的语句作为一个整体来执行,从而避免脏数据的产生。
public class Counter implements Runnable { private int counter = 200; public void run() { for (int i = 0; i < 50; i++) { synchronized(this) { counter -= 2; try {Thread.sleep(10);}catch(Exception e){} System.out.print(counter + " "); } } } }
public class TestThreadSync { public static void main(String[] args) { Counter c = new Counter(); Thread t1 = new Thread(c); Thread t2 = new Thread(c); t1.start(); t2.start(); } }
-
每个对象都有一个与其相关联的标记
-
synchronized语句块可以与这个标记交互
-
执行synchronized语句块开始时,对象的锁标记被线程持有。
-
当synchronized语句块结束后,锁标记被释放回对象。这包括以下情况:
- 线程正常执行至语句块的末端
- 语句块中抛出了异常
- 在语句块中遇到break语句或return语句
-
this表示对象自身,可被用在synchronized语句中。
-
必须注意:需要同步的线程,必须访问同一个锁对象,才能实现同步的效果
-
对易损坏数据的所有访问都应该被同步
-
受synchronized保护的易损坏数据都应标记为private
-
下面两个代码片断是等效的,但第一种方式会有更好的性能:
public void run() { synchronized(this) { // 同步代码块 //…… } }
public synchronized void run() { // 同步方法锁 // …… } // 如果是实例方法,则默认的锁对象是this // 如果是静态方法,则默认的锁对象是当前类.class
public class SellTicketDemo1{ public static void main(String[] args)throws IOException{ //加载配置文件里的count的对应值 Properties p=new Properties(); p.load(new FileReader("ticket.properties")); //获取键对应的值 String count=p.getProperty("count"); //创建代表票类的对象 Ticket t=new Ticket(); //设置票数 t.setCount(Integer.parseInt(count)); //创建四个Seller类(Runnable实现类)对象---四个售票员 //四个对象都是由相同的t对象构建而来,共享t对象的票数 Seller s1=new Seller(t); Seller s2=new Seller(t); Seller s3=new Seller(t); Seller s4=new Seller(t); //创建四个线程对象 //指定线程对象的名称 Threadt1=newThread(s1,"A"); Threadt2=newThread(s2,"B"); Threadt3=newThread(s3,"C"); Threadt4=newThread(s4,"D"); //开启线程 t1.start(); t2.start(); t3.start(); t4.start(); } } //代表线程的任务信息(卖票过程) class Seller implements Runnable{ //表示票数 //staticintcount=100; //引入代表票的类的对象 Ticket t; //有参构造---保证创建的对象共享同一个Ticket类的对象 public Seller(Ticket t){ this.t=t; } //卖票具体过程--重写run方法 @Override public void run(){ //循环实现重复卖票过程 while(true){ synchronized(t){//把当前参与的线程对象共享进来 //指定都是方法区的资源(方法区的资源是被所有的线程对象共享) //synchronized(Math.class){//Seller.classString.class //循环结束条件 if(t.getCount()==0) break; //卖一张票---设置新的票数 t.setCount(t.getCount()-1); //输出卖票信息 //Thread.currentThread()---当前正在执行线程对象 //getName()---线程对象的名称 System.out.println(Thread.currentThread().getName()+ "卖了一张票,还剩"+t.getCount()+"张票。。。"); //线程休眠 try{ Thread.sleep(10); }catch(InterruptedException e){ e.printStackTrace(); } } } } } //代表票的类 class Ticket{ //属性--票数 private int count; public int getCount(){ return count; } public void setCount(int count){ this.count=count; } }
线程的锁池阻塞状态
- 当线程执行synchronized语句时,如果不能获得被同步对象的锁标记,则进入该对象的锁池等待
- 当对象的锁标记可用时,锁池中的线程将回到可运行状态,参
- 如果有多个线程尝试获取相同的锁标记而处于阻塞状态,当锁标记被归还时,多个线程是争抢这个锁标记,而不是先到先得。这种情况也成为 非公平锁。
练习
-
修改Counter程序,使其实例数据在线程下安全
-
编写一个模拟银行取款程序,假定账户余额为10000元,有两个线程分别40次取款,每次取款100元(即从账户中减去相应的金额)。程序需要保证账户余额的准确性。
同步其他资源对象
-
在使用synchronized语句时,除了同步this对象外,还可同步其他资源对象。
public class Counter { private int counter = 200; public int getCounter() { return counter; } public void decCounter() { counter -= 2; } }
public class CounterUser implements Runnable { private Counter c; public CounterUser(Counter c) { this.c = c; } public void run() { for (int i = 0; i < 50; i++) { synchronized(c) { c.decCounter(); try {Thread.sleep(10);}catch(Exception e){} System.out.print(c.getCounter() + " "); } } } }
public class TestThreadSyncObject { public static void main(String[] args) { Counter c = new Counter(); CounterUser cu1 = new CounterUser(c); Thread t1 = new Thread(cu1); CounterUser cu2 = new CounterUser(c); Thread t2 = new Thread(cu2); t1.start(); t2.start(); } }
练习
-
编写一个Account类,包含id、name和balance属性,创建Account对象,余额为10000元;
-
编写有两个线程的程序,模拟取款,分别40次取款,每次取款100元(即从账户中减去相应的金额)。程序需要保证Account对象余额的准确性。
死锁
- 死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
线程的交互
- 有些情况下,不同线程所执行的任务之间有着某些方面的联系,可能需要在它们之间安排一些交互,即线程交互。
- 考虑这样的场景:现有一个银行账户资源,用户A(代表线程1)向账户中存款,用户B(代表线程2)从账户中取款。
- 在访问账户时,两个线程除了需要同步外,对于用户B,在取款时还须判断账户余额是否满足。如果余额不足,则需要等待用户A存款,且余额充足后才能进行。
- 从线程角度来说,针对账户这一资源,用户A和用户B存在以下交互情形:
- 用户B判断如果账户余额不足,则进入等待状态;否则执行取款;
- 用户A向账户存款后,通知用户B结束等待,执行第1步。
- java.lang.Object 类提供了两个方法用于线程间交互:
- wait :使调用该方法的线程进入等待状态
- notify:唤醒因执行wait方法而进入等待状态的线程
- 如果一个线程在某个交会点对象x上调用了wait方法,那么该线程会暂停执行,直到另一个线程在同一个交会点对象x上调用了notify 方法。
使线程阻塞 — wait方法
-
wait方法会使当前线程进入阻塞状态,直到该线程得到唤醒通知,或到达指定的时间。
-
因调用wait方法而阻塞的线程,共有三种唤醒方式:
- 通知唤醒:当收到来自notify或notifyAll方法的唤醒通知后
- 主动唤醒:当到达指定阻塞时间(由参数指定)后
- 中断唤醒:被另一个线程调用了interrupt方法,因此而收到InterruptedException唤醒信号。
-
wait和notify方法仅能在所访问对象的同步块内被调用。
-
当线程调用 wait方法进入等待状态时,会释放对象锁标记。
public class Counter implements Runnable {
private int counter = 0;
public void setCounter(int counter) {
this.counter = counter;
}
public synchronized void run() {
if (counter < 100) {
try {
this.wait();
} catch (InterruptedException e) { }
}
counter -= 100;
System.out.println("counter = " + counter);
System.out.println("Counter线程结束");
}
}
public class TestThreadWait {
public static void main(String[] args) {
Counter c = new Counter();
Thread t = new Thread(c);
t.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
synchronized (c) {
c.setCounter(150);
c.notify();
}
System.out.println("main线程结束");
}
}
- 当一个线程调用了指定对象的wait方法时,该线程会被置于此对象的等待池中;同时,线程会自动释放此对象的锁标记。
- 当调用了指定对象的notify方法时,该对象等待池中的某一个线程会被移到锁池;线程在锁池中继续等待,直到该对象锁标记可用。
- notifyAll方法会将指定对象等待池中的所有线程移至锁池。
- 在调用notify方法时,无须考虑等待池中是否有线程正在等待。如果此时并没有等待线程,该调用将不起任何作用。
练习
-
编写一个Account类,包含id、name和balance属性,创建Account对象,余额为0元;
-
编写有两个线程的程序,第一个模拟取款1000元(即从账户中减去相应的金额), 第二个模拟存款1000元(即向账户中增加相应的金额), 程序需要保证Account对象余额的准确性。
线程综合示例 — 生产与消费
- 对于多个不同客户端访问同一资源的情形,最典型的案例就是生产者-消费者示例。
- 在多线程编程方面,多个线程可能需要访问同一个资源对象。从线程角度而言:
- 创建或添加资源的线程称为生产者线程
- 删除或消耗资源的线程称为消费者线程
public class Counter {
private int counter = 0;
public synchronized int increase(int n) {
counter += n;
this.notify();
System.out.printf("生产:counter=%-2d n=%-2d\n", counter, n);
return counter;
}
public synchronized int decrease(int n) {
while (counter < n) {
try {
this.wait();
} catch (InterruptedException e) {
}
}
counter -= n;
System.out.printf("消费:counter=%-2d n=%-2d\n", counter, n);
return counter;
}
}
public class Producer implements Runnable {
private Counter counter;
public Producer(Counter counter) {
this.counter = counter;
}
public void run() {
for (int i = 0; i < 50; i++) {
counter.increase((int)(Math.random()*10));
try {
Thread.sleep((int)(Math.random()*100));
} catch (InterruptedException e) {
}
}
}
}
public class Consumer implements Runnable {
private Counter counter;
public Consumer(Counter counter) {
this.counter = counter;
}
public void run() {
for (int i = 0; i < 50; i++) {
counter.decrease((int)(Math.random()*8));
try {
Thread.sleep((int)(Math.random()*100));
} catch (InterruptedException e) {
}
}
}
}
public class TestThreadPC {
public static void main(String[] args) {
Counter counter = new Counter();
Producer p = new Producer(counter);
Thread t1 = new Thread(p);
Consumer c = new Consumer(counter);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}
练习
-
编写一个Account类,包含id、name和balance属性,创建Account对象,余额为0元;
-
分别编写有两个线程的程序,一个模拟存款,一个模拟取款,两次存款或取款操作间隔时间为200ms内的随机值。分别20次进行存取款,每次存100-500元不等,取100-300元不等。程序需要保证Account对象余额的准确性。