文章目录
一、进程与线程
- 进程
正在执行的应用程序,是系统进行资源分配和调用的独立单位;
每一个进程都有它自己的内存空间和系统资源。
多进程存在能够提高CPU的利用率 - 线程
进程的执行单元,执行路径;
一个应用程序只有一条执行路径则为单线程程序
一个应用程序有多条条执行路径则为多线程程序
多线程的存在能够提高应用程序的使用率
小问题:Java程序的运行原理及JVM的启动是多线程的吗?
答:A:Java命令去启动JVM,JVM会启动一个进程,该进程会启动一个主线程。
B:JVM的启动是多线程的,因为它最低有两个线程启动了,主线程和垃圾回收线程
二、线程的生命周期图
三、线程的三种创建方式
-
1.继承Thread类
-
2.实现Runnable接口
-
3.实现Callable接口(基于线程池,第九节讲解)
代码演示
public class MyThread extends Thread {
//重写run方法
@Override
public void run() {
//线程中需要执行的逻辑
for (int i = 0; i < 5; i++) {
System.out.println("方式1:"+i);
}
}
}
public class ThreadTest {
public static void main(String[] args) {
creatThread();
}
/**
* 线程的创建
*/
public static void creatThread() {
//方式1(继承Thread类,重写run方法)
MyThread myThread = new MyThread();
//方式2(实现Runnable接口)
Runnable r = () -> {
for (int i = 0; i < 5; i++) {
//线程中需要执行的逻辑
System.out.println("方式2:"+i);
}
};
Thread thread = new Thread(r);
//启动2个线程
myThread.start();
thread.start();
}
}
运行结果:
方式1:0
方式1:1
方式1:2
方式1:3
方式2:0
方式2:1
方式2:2
方式2:3
方式2:4
方式1:4
四、线程的常见方法
1.线程的命名
-
1.通过setName()方法
-
2.通过构造方法
代码演示:
public static void main(String[] args) {
threadSetName();
}
/**
* 线程命名
*/
public static void threadSetName() {
//1.通过setName方法
MyThread thread1 = new MyThread();
thread1.setName("Thread-1");
System.out.println(thread1.getName()); //结果:Thread-1
Thread thread2 = new Thread( ()->{} );
thread2.setName("Thread-2");
System.out.println(thread2.getName()); //结果:Thread-2
//2.通过构造方法
// MyThread需要重写构造方法
MyThread thread3 = new MyThread("Thread-3");
System.out.println(thread3.getName()); //结果:Thread-3
Thread thread4 = new Thread( ()->{} ,"Thread-4");
System.out.println(thread4.getName()); //结果:Thread-4
}
MyThread类:
public class MyThread extends Thread {
public MyThread() {}
public MyThread(String name) {
super(name);
}
@Override
public void run() {
//....
}
}
2.线程的休眠
代码演示:
public static void main(String[] args) {
threadSleep();
}
/**
* 线程休眠
*/
public static void threadSleep() {
//从0-9,依次打印,每隔一秒打印一个
Thread thread = new Thread(()->{
for (int i = 0; i < 10; i++) {
System.out.println(i);
try {
//静态方法,线程将从运行状态到阻塞状态
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
}
3.线程优先级
线程优先级:默认是5,范围是1-10;数字越大优先级越高;
设置线程的优先级是改变线程抢到CPU时间片的概率,所以不一定优先级高的线程就一定能抢到时间片
代码演示:
public static void main(String[] args) {
threadPriority();
}
/**
* 线程的优先级
*/
public static void threadPriority(){
Runnable r = () ->{
for (int i = 0; i < 5; i++) {
//Thread.currentThread() 获取当前线程
System.out.println(Thread.currentThread().getName()+":"+i);
}
};
Thread thread1 = new Thread(r,"Thread-1");
Thread thread2 = new Thread(r,"Thread-2");
//设置优先级
thread1.setPriority(10);
thread2.setPriority(1);
thread1.start();
thread2.start();
}
运行结果:
Thread-1:0
Thread-2:0
Thread-1:1
Thread-2:1
Thread-1:2
Thread-1:3
Thread-1:4
Thread-2:2
Thread-2:3
Thread-2:4
4.线程礼让
线程礼让:使线程从运行状态回到就绪状态,重新争夺CPU时间片;
线程礼让后CPU时间片不一定会被其他线程争夺到;
代码演示
public static void main(String[] args) {
threadYield();
}
/**
* 线程礼让
*/
public static void threadYield(){
Runnable r = () ->{
for (int i = 0; i < 5; i++) {
if(i==2){
//线程礼让
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+":"+i);
}
};
Thread thread1 = new Thread(r,"Thread-1");
Thread thread2 = new Thread(r,"Thread-2");
thread1.start();
thread2.start();
}
运行结果:
Thread-1:0
Thread-1:1
Thread-2:0
Thread-1:2
Thread-2:1
Thread-1:3
Thread-1:4
Thread-2:2
Thread-2:3
Thread-2:4
5.线程加入
线程加入:需等待线程执行完毕( 调用thread.join() )或执行给定时间( 调用thread.join(时间) )后再执行其他线程。
此方法可控制线程的执行顺序。
代码演示:
public static void main(String[] args) {
threadJoin();
}
/**
* 线程加入
*/
public static void threadJoin(){
Runnable r = () ->{
for (int i = 0; i < 5; i++) {
if(i==2){
//线程礼让
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+":"+i);
}
};
Thread thread1 = new Thread(r,"Thread-1");
Thread thread2 = new Thread(r,"Thread-2");
Thread thread3 = new Thread(r,"Thread-3");
thread1.start();
try {
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.start();
thread3.start();
}
运行结果:
Thread-1:0
Thread-1:1
Thread-1:2
Thread-1:3
Thread-1:4
Thread-2:0
Thread-3:0
Thread-2:1
Thread-3:1
Thread-2:2
Thread-2:3
Thread-2:4
Thread-3:2
Thread-3:3
Thread-3:4
6.守护(后台)线程
当所有的线程都为守护线程时,java虚拟机退出(程序结束);
代码演示
public static void main(String[] args) {
threadDaemon();
}
/**
* 守护线程
*/
public static void threadDaemon(){
Runnable r = () ->{
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
};
Thread thread1 = new Thread(r,"Thread-1");
Thread thread2 = new Thread(r,"Thread-2");
//设置守护线程
thread1.setDaemon(true);
thread2.setDaemon(true);
thread1.start();
thread2.start();
Thread.currentThread().setName("主线程");
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
运行结果
主线程:0
Thread-1:0
Thread-2:0
主线程:1
Thread-2:1
Thread-1:1
主线程:2
Thread-1:2
Thread-2:2
Thread-1:3
Thread-1:4
Thread-1:5
Thread-1:6
Thread-1:7
Thread-2:3
Thread-2:4
Thread-1:8
Thread-2:5
Thread-2:6
7.中断线程
调用thread.interrupt()方法终止线程,该方法将终止线程状态,并抛出异常;
代码演示:
public static void main(String[] args) {
threadInterrupt();
}
/**
* 线程终止
*/
public static void threadInterrupt() {
Thread thread = new Thread(() -> {
System.out.println("时间1:"+ LocalDateTime.now());
//休眠10秒
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("时间2:"+ LocalDateTime.now());
});
thread.start();
//主线程休眠3秒
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//终止线程
thread.interrupt();
}
运行结果
时间1:2019-10-10T21:20:42.473
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.fx.thread.ThreadTest.lambda$threadInterrupt$8(ThreadTest.java:197)
at java.lang.Thread.run(Thread.java:748)
时间2:2019-10-10T21:20:45.464
五、临界资源问题
代码演示
public class SellTicket {
//总票数
public static int tickets = 10;
public static void main(String[] args) {
Runnable r = () -> {
while (SellTicket.tickets>0){
System.out.println(Thread.currentThread().getName() +"卖出一张票;剩余票数:"
+ (--SellTicket.tickets) + "张");
}
};
//模拟售票员
Thread thread1 = new Thread(r,"thread-1");
Thread thread2 = new Thread(r,"thread-2");
Thread thread3 = new Thread(r,"thread-3");
Thread thread4 = new Thread(r,"thread-4");
//开始售票
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
运行结果:
thread-1卖出一张票;剩余票数:9张
thread-2卖出一张票;剩余票数:8张
thread-3卖出一张票;剩余票数:6张
thread-1卖出一张票;剩余票数:7张
thread-1卖出一张票;剩余票数:4张
thread-1卖出一张票;剩余票数:3张
thread-1卖出一张票;剩余票数:2张
thread-1卖出一张票;剩余票数:1张
thread-1卖出一张票;剩余票数:0张
thread-3卖出一张票;剩余票数:4张
thread-2卖出一张票;剩余票数:5张
thread-3卖出一张票;剩余票数:2张
thread-4卖出一张票;剩余票数:3张
thread-3卖出一张票;剩余票数:0张
thread-2卖出一张票;剩余票数:1张
thread-4卖出一张票;剩余票数:-1张
-
出现的问题
由代码运行结果可知:票数不是我们想象中一张一张的递减,而且会有重复票的卖出,甚至负数票,显然这是不安全,不符合实际的; -
出现这种情况的原因是什么呢?
想象,在临界资源的操作(tickets)上,当thread-1抢到时间片,正要执行“–SellTicket.tickets”而还未执行时,被thread-2抢走了时间片,在这时两者拿到的“tickets”值相同,打印出来的票数就相同了(重复票的卖出问题);而线程抢到时间片的时间和打印操作执行的时间交错也就形成了如上的运行结果; -
如何解决呢?
我们需要引入锁的概念。将对临界资源的操作上锁:当有线程进入时,上锁,其他线程等待,当操作完成后,释放锁,这样线程之间就不会出现多个线程同时操作临界资源的情况,问题得以解决。
六、锁
java中的锁有三种实现方式:同步代码块,同步方法和显示锁;
同步代码块
同步代码块synchronized ("") {},()中是锁参数,该锁可以是对象锁,也可以是类锁,但要注意,操作临界资源的线程之间的锁是一致的;
synchronized (new Object() ) {},这种写法的锁参数就是不一致的锁,没有同步效果;
代码演示
注:当tickets为1时,线程进入代码块,执行 tickets-- 操作,票数变为0,而此时可能有线程在循环内,同步代码块外等待,释放锁后,等待的线程操作的票数就为零了,所以会出现负数票。因此,我们在同步代码块中加上判断票数的操作;
public class SellTicket {
//总票数
public static int ticket = 10;
public static void main(String[] args) {
Runnable r = () -> {
while (SellTicket.ticket > 0) {
synchronized ("") {
//防止出现负数票
if(SellTicket.ticket <= 0){
return;
}
System.out.println(Thread.currentThread().getName() + "卖出一张票;剩余票数:"
+ (--SellTicket.ticket) + "张");
}
}
};
//模拟售票员
Thread thread1 = new Thread(r, "thread-1");
Thread thread2 = new Thread(r, "thread-2");
Thread thread3 = new Thread(r, "thread-3");
Thread thread4 = new Thread(r, "thread-4");
//开始售票
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
运行结果:
thread-1卖出一张票;剩余票数:9张
thread-1卖出一张票;剩余票数:8张
thread-1卖出一张票;剩余票数:7张
thread-1卖出一张票;剩余票数:6张
thread-2卖出一张票;剩余票数:5张
thread-2卖出一张票;剩余票数:4张
thread-2卖出一张票;剩余票数:3张
thread-2卖出一张票;剩余票数:2张
thread-2卖出一张票;剩余票数:1张
thread-2卖出一张票;剩余票数:0张
同步方法
我们可以将同步代码块中的代码提取到方法中,而如果一个方法中都是同步操作,就可以用synchronized 修饰,使方法称为同步方法;
静态同步方法的锁是 类锁(当前类.class)
非静态同步方法的锁是 对象锁 (this)
代码演示:
public class SellTicket {
//总票数
public static int ticket = 10;
//同步方法
public synchronized static void sellTicket(){
//防止出现负数票
if(SellTicket.ticket <= 0){
return;
}
System.out.println(Thread.currentThread().getName() + "卖出一张票;剩余票数:"
+ (--SellTicket.ticket) + "张");
}
public static void main(String[] args) {
Runnable r = () -> {
while (SellTicket.ticket > 0) {
sellTicket();
}
};
//模拟售票员
Thread thread1 = new Thread(r, "thread-1");
Thread thread2 = new Thread(r, "thread-2");
Thread thread3 = new Thread(r, "thread-3");
Thread thread4 = new Thread(r, "thread-4");
//开始售票
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
运行结果
thread-1卖出一张票;剩余票数:9张
thread-3卖出一张票;剩余票数:8张
thread-3卖出一张票;剩余票数:7张
thread-3卖出一张票;剩余票数:6张
thread-3卖出一张票;剩余票数:5张
thread-3卖出一张票;剩余票数:4张
thread-3卖出一张票;剩余票数:3张
thread-3卖出一张票;剩余票数:2张
thread-3卖出一张票;剩余票数:1张
thread-3卖出一张票;剩余票数:0张
显示锁
代码演示:
public class SellTicket {
//总票数
public static int ticket = 10;
//创建锁对象
public static Lock lock = new ReentrantLock();
public static void main(String[] args) {
Runnable r = () -> {
while (SellTicket.ticket>0){
lock.lock(); // 上锁
if(SellTicket.ticket <= 0){
return;
}
System.out.println(Thread.currentThread().getName() + "卖出一张票;剩余票数:"
+ (--SellTicket.ticket) + "张");
lock.unlock(); //释放锁
}
};
//模拟售票员
Thread thread1 = new Thread(r, "thread-1");
Thread thread2 = new Thread(r, "thread-2");
Thread thread3 = new Thread(r, "thread-3");
Thread thread4 = new Thread(r, "thread-4");
//开始售票
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
运行结果
thread-1卖出一张票;剩余票数:9张
thread-3卖出一张票;剩余票数:8张
thread-3卖出一张票;剩余票数:7张
thread-3卖出一张票;剩余票数:6张
thread-3卖出一张票;剩余票数:5张
thread-3卖出一张票;剩余票数:4张
thread-3卖出一张票;剩余票数:3张
thread-3卖出一张票;剩余票数:2张
thread-3卖出一张票;剩余票数:1张
thread-3卖出一张票;剩余票数:0张
七、死锁问题及解决办法
死锁:是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待现象
代码演示:
public class Deadlock {
public static void main(String[] args) {
Thread thread1 = new Thread(()->{
synchronized ("A"){
System.out.println("线程1得到A锁,等待B锁");
synchronized ("B"){
System.out.println("线程1同时得到A、B锁");
}
}
});
Thread thread2 = new Thread(()->{
synchronized ("B"){
System.out.println("线程2得到B锁,等待A锁");
synchronized ("A"){
System.out.println("线程2同时得到A、B锁");
}
}
});
thread1.start();
thread2.start();
}
}
运行结果(进入死锁):
问题分析:当线程1得到A锁时,线程B又获取到B锁,两者由于未执行完逻辑都不释放锁,相互等待,造成死锁现象;
如何解决:如果线程1得到A锁后,现将A锁释放,线程2就可以顺利执行,但线程2需要唤醒线程1继续执行,这样死锁问题就得以解决;
代码演示:
public class Deadlock {
public static void main(String[] args) {
Thread thread1 = new Thread(()->{
synchronized ("A"){
System.out.println("线程1得到A锁,等待B锁");
//释放“A”锁
try {
"A".wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized ("B"){
System.out.println("线程1同时得到A、B锁");
}
}
});
Thread thread2 = new Thread(()->{
synchronized ("B"){
System.out.println("线程2得到B锁,等待A锁");
synchronized ("A"){
System.out.println("线程2同时得到A、B锁");
//唤醒
"A".notify();
}
}
});
thread1.start();
thread2.start();
}
}
运行结果:
线程1得到A锁,等待B锁
线程2得到B锁,等待A锁
线程2同时得到A、B锁
线程1同时得到A、B锁
八、等待唤醒机制
Object类中提供了以下三种方法:
1.wait() : 等待,当前线程释放自己的锁标记,并且让出CPU资源,使当前线程进入等待队列中;
2.notify() :唤醒,唤醒等待队列中,等待调用锁对象的一个线程,使这个线程进入锁池;
3.notifyAll() 唤醒所有,唤醒等待队列中,等待调用锁对象的所有线程,并使这些线程进入锁池;
生产者消费者例子:用有两个线程来模拟“电影票的生产和售出”,一个生产者线程,一个消费者线程。
需求:
1.生产者生产一个,消费者就消费一个。
2.若未生产,消费者需等待生产者生产。
3.若未消费,生产者需等待。
4.两种电影票的轮循生产售出
代码如下:
/**电影票类*/
public class MovieTicket {
private String name; //电影名
private LocalDate date; //播出时间
private boolean flag; //是否可被消费,初始值false(不可消费)
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public LocalDate getDate() {
return date;
}
public void setDate(LocalDate date) {
this.date = date;
}
public boolean getFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
/**生产者*/
public class Producer implements Runnable {
private MovieTicket movieTicket;
private int x = 0;
public Producer(MovieTicket movieTicket) {
this.movieTicket = movieTicket;
}
@Override
public void run() {
while (true) {
synchronized (movieTicket) {
if(movieTicket.getFlag()){ //如果可被消费(即已经生产但未被消费),则等待
try {
movieTicket.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if ((x & 1) == 0) { //x为偶数生产我和我的祖国
System.out.println("生产:我和我的祖国");
movieTicket.setName("我和我的祖国");
movieTicket.setDate(LocalDate.of(2019, 10, 1));
} else { //x为奇数生产中国机长
System.out.println("生产:中国机长");
movieTicket.setName("中国机长");
movieTicket.setDate(LocalDate.of(2019, 10, 2));
}
x++;
movieTicket.setFlag(true); //设置可被消费
movieTicket.notify(); //唤醒消费者
}
}
}
}
/**消费者*/
public class Consumer implements Runnable {
private MovieTicket movieTicket;
public Consumer(MovieTicket movieTicket) {
this.movieTicket = movieTicket;
}
@Override
public void run() {
while (true){
synchronized (movieTicket){
if(!movieTicket.getFlag()){ //如果不可消费,则等待消费者生产
try {
movieTicket.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("卖出:"+movieTicket.getName()+":"+movieTicket.getDate()); //消费一次
movieTicket.setFlag(false); //设置为不可消费
movieTicket.notify(); //唤醒生产者
}
}
}
}
/**测试类*/
public class Test {
public static void main(String[] args) {
//创建MovieTicket对象
MovieTicket movieTicket = new MovieTicket();
//设置生产者与消费者
Producer producer = new Producer(movieTicket);
Consumer consumer = new Consumer(movieTicket);
//创建线程
Thread thread1 = new Thread(producer);
Thread thread2 = new Thread(consumer);
//开启
thread2.start();
thread1.start();
}
}
运行结果(一部分):
生产:我和我的祖国
卖出:我和我的祖国:2019-10-01
生产:中国机长
卖出:中国机长:2019-10-02
生产:我和我的祖国
卖出:我和我的祖国:2019-10-01
生产:中国机长
卖出:中国机长:2019-10-02
生产:我和我的祖国
卖出:我和我的祖国:2019-10-01
生产:中国机长
卖出:中国机长:2019-10-02
生产:我和我的祖国
卖出:我和我的祖国:2019-10-01
生产:中国机长
卖出:中国机长:2019-10-02
生产:我和我的祖国
卖出:我和我的祖国:2019-10-01
九、线程池
程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池。
线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。
从JDK5开始,Java内置支持线程池:
线程池的创建与使用
JDK5新增了 Executors工厂类来产生线程池,有如下几个方法:
public static ExecutorService newCachedThreadPool() 创建一个线程池
public static ExecutorService newFixedThreadPool(int nThreads) 创建一个指定容量的线程池
public static ExecutorService newSingleThreadExecutor() 创建一个容量为1的线程池
这些方法的返回值是ExecutorService对象,该对象表示一个线程池,可以执行Runnable对象或者Callable对象代表的线程。它提供了如下方法
Future<?> submit(Runnable task) 提交并执行实现Runable接口的线程类
Future submit(Callable task) 提交并执行实现Callable接口的线程类
代码演示:
/**实现Runnable接口的线程类*/
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
public class Demo1 {
public static void main(String[] args) {
//创建容量为2的线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
//提交并运行两个线程
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
//结束线程池(若不结束,程序将不会停止,线程存在池中)
pool.shutdown();
}
}
运行结果:
pool-1-thread-1:0
pool-1-thread-1:1
pool-1-thread-2:0
pool-1-thread-1:2
pool-1-thread-2:1
pool-1-thread-1:3
pool-1-thread-2:2
pool-1-thread-1:4
pool-1-thread-2:3
pool-1-thread-2:4
使用Callable接口创建线程
Callable:是带泛型的接口。 这里指定的泛型其实是call()方法的返回值类型。
代码演示:
/**实现Callable接口的线程类,用于求和*/
public class MyCallable implements Callable<Integer> {
private Integer startNum;
private Integer endNum;
public MyCallable(Integer startNum, Integer endNum) {
this.startNum = startNum;
this.endNum = endNum;
}
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = startNum;i<=endNum;i++){
sum+=i;
}
return sum;
}
}
public class Demo2 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建容量为2的线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
//提交并运行两个线程
Future<Integer> future1 = pool.submit(new MyCallable(1, 10));
Future<Integer> future2 = pool.submit(new MyCallable(1, 100));
System.out.println(future1.get());
System.out.println(future2.get());
//结束线程池(若不结束,程序将不会停止,线程存在池中)
pool.shutdown();
}
}
运行结果
55
5050