@
目录
一、基本概念的理解
1、程序、进程、线程
- 程序(program):一组指令的集合,一段静态的代码。
- 进程(progress):程序的一次执行过程,或正在运行的一个程序。
- 线程(thread):一个进程内部的一条执行路径。
2、单核CPU与多个CPU
- 单核CPU:只有一个CPU芯片。一个极短的时间内只能执行一个线程任务。由于在线程之间切换的时间极短,给人多线程的假象。
- 多核CPU:多个CPU芯片。
3、串行、并行、并发
- 串行:一个线程执行完再执行下一个线程。
- 并行:多个CPU同时执行多个任务。
- 并发:一个CPU同时执行多个任务,交替执行,抢占CPU时间。
二、多线程
1、java中的多线程举例
- 一个Java应用程序java.exe,其实至少有三个线程:main()主线程,gc() 垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
2、多线程优点
- 提高应用程序的响应。命令行下,我们一般一个时间只做一件事,但是图形界面下则不是,因此有必要来使用多线程保证各个任务的响应速度。
- 提高CPU利用率。
- 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。比如
3、何时需要多线程
- 程序需要同时执行两个或多个任务。
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
- 需要一些后台运行的程序时。
三、多线程的创建和使用★★★★★
1、方式一:继承Thread类(JDK1.5之前的两种之一)
- 代码实现
package com.thread;
public class ThreadTest1 {
public static void main(String[] args) {
new MyThread1().start();
}
}
class MyThread1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(MyThread1.currentThread().getName() + ":" + i);
}
}
}
//当某个线程只适用了一次的时候,也可以使用匿名类匿名对象的方式来创建多线程
package com.thread;
public class ThreadTest2 {
public static void main(String[] args) {
new Thread(){
@Override
public void run() {
System.out.println("我是子线程");
}
}.start();
}
}
- 步骤总结
1) 定义子类继承Thread类。
2) 子类中重写Thread类中的run方法。
3) 创建Thread子类对象,即创建了线程对象。
4) 调用线程对象start方法:启动线程,调用run方法。
- 注意:
- 启动多线程,必须调用start方法。如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式
- 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上 的异常“IllegalThreadStateException”。
- 因此,如果我们想两个线程同时做一件事,要new两个对象
- 因此,如果我们想两个线程同时做两件事,重写一个run()是不够的,这是我们就要创建2个类继承,然后再分别重写run()。
-
Thread类中的方法(详细解释看API文档)
-
线程的调度:时间片策略、抢占式
- Java中,同优先级线程组成先进先出队列(先到先服务),使用时间片策略
- 对高优先级,使用优先调度的抢占式策略
-
线程的优先级
- 优先级等级
- MAX_PRIORITY:10
- MIN _PRIORITY:1
- NORM_PRIORITY:5
- 方法
- getPriority() :返回线程优先值
- setPriority(int newPriority) :改变线程的优先级
2、方式二:实现Runnable接口(JDK1.5之前的两种之一)
-
代码实现
public class ThreadTest3 { public static void main(String[] args) { MyThread3 myThread3 = new MyThread3(); Thread t1 = new Thread(myThread3); t1.start(); } } class MyThread3 implements Runnable{ @Override public void run() { System.out.println("我是子线程"); } } //当某个线程只适用了一次的时候,也可以使用匿名类匿名对象的方式来创建多线程 new Thread(new Runnable() { @Override public void run() { } }).start();
-
步骤总结
1. 创建一个实现了Runnable接口的类 2. 实现类去实现Runnable中的抽象方法:run() 3. 创建实现类的对象 4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象 5. 通过Thread类的对象调用start()
-
比较两种创建线程的方式
-
开发中:优先选择:实现Runnable接口的方式
原因:
- 实现的方式没有类的单继承性的局限性
- 实现的方式更适合来处理多个线程有共享数据的情况。
-
联系:
- public class Thread implements Runnable
- 相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。
-
3、方式三:实现Callable接口
4、方式四:使用线程池(开发中主要是用这个)
四、线程的生命周期
1、线程的生命周期★★★★★
- 说明
- 一个线程的最终状态是死亡,不能停在阻塞。
五、线程的同步
1、线程安全问题举例和解决
- 以卖票窗口为例,三个窗口本来就存在重票、错票(票号小于等于0),只是概率较小。可以通过sleep方法来使这种效果更加明显。这就是线程安全问题
class Window implements Runnable{
private int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0){
//在这里让刚进入的线程阻塞100ms,错票的概率就会增加
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("售票,票号为: " + ticket);
//如果在这里让刚进入的线程阻塞100ms,重票的概率就会增加
ticket--;
}else{
break;
}
}
}
}
public class WindowTest {
public static void main(String[] args) {
Window w1 = new Window();
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
t1.setName("窗口1");
t1.setName("窗口2");
t1.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
- 解决方案:当一个线程a操作ticket的时候,其他线程不能进来,知道a完事了,及时是a阻塞了,也不能进来。(和上厕所把门锁住一个道理,逃~)在java中用同步机制来解决。
- 对于继承和实现的方式,同步锁解决线程安全问题时,是重要牢记:继承会创建多个对象,一般都要用static来修饰属性和方法的。
2、同步代码块
-
语法
synchronized(锁){ 需要被同步的代码 }
-
说明
- 锁:即同步监视器,任何一个对象都可以充当锁。但是要求,多个线程必须共用一把锁(就是说,new对象这一步,必须是只发生一次)
- 被同步的代码:这一部分是操作共享数据的代码。但是不能包多--->如果我们把while也包起来,就成了一个窗口卖所有的票。
-
对window的改进
- 对继承的方式的改进
/* 说明: 1、继承的方式的特点就是我们需要new好几个对象,所以需要这些对象都共享票数和锁,声明为static即可解决这个问题。 2、还可以用Window1.class来充当锁。 */ class Window1 extends Thread{ private static int ticket = 100; static Object obj = new Object(); @Override public void run() { while (true) { synchronized (obj) { if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("ticket_number=" + ticket); ticket--; }else{ break; } } } } } public class Window_Thread_Security1 { public static void main(String[] args) { Window1 w1 = new Window1(); Window1 w2 = new Window1(); Window1 w3 = new Window1(); w1.start(); w2.start(); w3.start(); } }
- 对实现方式的改进
/* 说明: 1、实现的方式的特点是只new了一个类的对象,因此票数和锁自动就是共享的。 2、还可以用this来充当锁。 */ class Window2 implements Runnable{ private int ticket = 100; @Override public void run() { while (true) { synchronized (this) { if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("ticket_number=" + ticket); ticket--; }else{ break; } } } } } public class Window_Thread_Security2 { public static void main(String[] args) { Window2 window2 = new Window2(); Thread t1 = new Thread(window2); Thread t2 = new Thread(window2); Thread t3 = new Thread(window2); t1.start(); t2.start(); t3.start(); } }
-
优缺点
- 优:解决了线程安全问题
- 缺:操作同步代码时,只有一个线程参与,相当于是单线程,效率低。
3、同步方法
-
语法:synchronized放在方法声明中,表示整个方法是同步方法
public synchronized void show (String name){ //方法体 }
-
对window问题的改进
- 对继承方式的改建
class Window3 extends Thread{ private static int ticket = 100; private static synchronized void show(){//因为我们这里的默认省略了锁, // 如果不加static,锁就是this,这显然多个对象对对应多个锁 //加了static以后,锁就成了Window3.class,是惟一的锁 if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("ticket_number=" + ticket); ticket--; } } @Override public void run() { while (true) { show(); } } } public class Window_Thread_Security3 { public static void main(String[] args) { Window1 w1 = new Window1(); Window1 w2 = new Window1(); Window1 w3 = new Window1(); w1.start(); w2.start(); w3.start(); } }
- 对实现方式的改进
class Window4 implements Runnable{ private int ticket = 100; private synchronized void show(){//这时,锁是this,只创建了一个Window4的对象,是唯一的. if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("ticket_number=" + ticket); ticket--; } } @Override public void run() { while (true) { show(); } } } public class Window_Thread_Security4 { public static void main(String[] args) { com.thread.Window2 window2 = new com.thread.Window2(); Thread t1 = new Thread(window2); Thread t2 = new Thread(window2); Thread t3 = new Thread(window2); t1.start(); t2.start(); t3.start(); } }
4、死锁
- 概念:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃 自己需要的同步资源,就形成了线程的死锁。出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
- 尽量避免死锁
- 专门的算法、原则。
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
- 死锁的演示
package com.thread3;
public class DeadLock {
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(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (s2){
s1.append("b");
s2.append("2");
System.out.println("s1 = " + s1);
System.out.println("s2 = " + 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 = " + s1);
System.out.println("s2 = " + s2);
}
}
}).start();
}
}
/*
输出结果有哪些可能
线程1先执行,线程2后执行:ab 12 adcd 1234
线程2先执行,线程1后执行:cd 34 cdab 3412
都阻塞:
*/
5、lock
-
介绍
- 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。(Lock是一个接口)
- ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
-
语法
class A{
private final ReentrantLock lock = new ReentrantLock();
public void method(){
lock.lock();//请求锁
try{
代码逻辑
}finnaly{
lock.unlock();//释放锁
}
}
}
- 用Lock解决window卖票的线程安全问题
class Window5 extends Thread{
private static int ticket = 100;
private static final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
lock.lock();
try {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("ticket_number=" + ticket);
ticket--;
}else{
break;
}
} finally {
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Window5 w1 = new Window5();
Window5 w2 = new Window5();
Window5 w3 = new Window5();
w1.start();
w2.start();
w3.start();
}
}
- 面试题
- synchronized 与 Lock的异同?
- 同:都是用于解决线程安全问题
- 异:①、Lock是显示锁,需要我们自己手动开启和关闭;synchronized是隐式锁,出了作用域就会自动释放锁。②、Lock只有代码块锁,synchronized有代码块锁和方法锁。③、Lock锁性能好,拓展性好。
- 如何解决线程安全问题,有几种方式?
- 两大类。一类是Lock,一类是synchronized
- synchronized 与 Lock的异同?
- 开发中优先使用:Lock--->同步代码块--->同步方法
六、线程的通信
- 线程通信的例子:使用两个线程打印 1-10。线程1, 线程2 交替打印
//1、要先分析是不是多线程为题;
//2、在分析是否有共享数据,有---同步机制来解决
class MyPrinter implements Runnable {
private int number = 1;
@Override
public synchronized void run() {
while (true) {
this.notify();
if (number <= 10) {
// this.notify();//如果我把notify放在这里,程序就不会自动结束了? 因为,放在这里,
//最后一个循环的情况下,打印机二带着10进入,可以notify打印机一,再下一轮,打印机一拿着11
//,无法进入if,无法notify打印机二,打印机二就一直阻塞在那里
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
public class ThreadCommunicationTest {
public static void main(String[] args) {
MyPrinter myPrinter = new MyPrinter();
Thread t1 = new Thread(myPrinter);
Thread t2 = new Thread(myPrinter);
t1.setName("打印机一");
t2.setName("打印机二");
t1.start();
t2.start();
}
}
- 涉及到的三个方法:
- wait():当前线程阻塞,并释放锁
- notify():当前线程唤醒被wait的其他一个线程,谁的优先级高就唤醒谁
- notifyAll():当前线程唤醒all被wait的线程
- 对这三个方法的说明
- 个方法必须使用在同步代码块或同步方法中。
- 三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会出现
IllegalMonitorStateException
异常。- 我这里用的是this;如果我自己new一个Object类,那么this就要改为Object类的对象。
- 三个方法是定义在
java.lang.Object
类中。
- 面试题:sleep() 和 wait()的异同?★★★★★
- 同:两者都可以使线程阻塞
- 异:
- 声明的位置不同:sleep()是声明在Thread类中;wait()是声明在Object类中
- 使用的位置不同:sleep()使用在任意位置;wait()使用在同步代码块或同步方法中
- 使用完了以后做的事不同:sleep()不会释放锁,wait()会释放锁
七、生产者和消费者问题
/**
* 线程通信的例子:使用两个线程打印 1-100。线程1, 线程2 交替打印
*
* 涉及到的三个方法:
* wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
* notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
* notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
*
* 说明:
* 1.wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
* 2.wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。
* 否则,会出现IllegalMonitorStateException异常
* 3.wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。
*
* 面试题:sleep() 和 wait()的异同?
* 1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
* 2.不同点:1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
* 2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
* 3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。
*
* @author shkstart
* @create 2019-02-15 下午 4:21
*/
class Number implements Runnable{
private int number = 1;
private Object obj = new Object();
@Override
public void run() {
while(true){
synchronized (obj) {
obj.notify();
if(number <= 100){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
try {
//使得调用如下wait()方法的线程进入阻塞状态
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}