线程
1、线程的相关概念
1.1、并行和并发
并行:在同一时刻,有多个任务在多个CPU上同时执行
并发:在同一时刻,有多个任务在单个CPU上交替执行
1.2、进程和线程
进程:就是在多任务管理系统中,每个独立执行的程序,进程就是”正在进行的程序“
线程:就是程序运行的基本单元。当操作系统执行一个程序时,会在系统中建立一个进程,该进程必须至少建立一个线程(这个线程被称为主线程)作为这个程序运行的入口点。因此,在操作系统中运行的任何程序都至少有一个线程。
2、什么是多线程?
是指从软件或者硬件上实现多个线程并发执行的技术。
具有多线程能力的计算机因为硬件支持而能够在同一时间执行多个线程,提升性能。
3、线程的使用
3.1、使用多线程的好处
提高程序的效率
3.2、多线程如何开发
3.2.1、线程启动方式之Thread类(java语言提供的线程类)
概述:java.lang.Thread是线程类,可以用来给进程创建线程处理任务使用,其中线程中有两个中要的方法:
- public void run():线程执行任务的方法,是线程启动后第一个执行的方法
- public void start():启动线程的方法,线程对象调用该方法后,Java虚拟机就会调用此线程的run方法
线程启动步骤:
- 创建一个子类继承Thread类(创建的子类也是线程类)
- 在子类中,编写让线程完成的任务(任务代码)
- 重写Thread类中的run方法(线程任务)
- 启动线程
示例:
创建一个子类线程:
public class MyThread extends Thread{
@Override
public void run() {
System.out.println("我的线程开始啦");
for (int i = 100; i < 200; i++) {
System.out.println("我的线程:" + i);
}
}
}
在main中启动子线程
public class ThreadDemo1 {
public static void main(String[] args) {
//创建一个线程对象
MyThread myThread = new MyThread();
//启动线程
myThread.start();
//主线程代码
for (int i = 0; i < 100; i++) {
System.out.println("主线程:" + i);
}
}
}
结果展示(展示部分):
主线程:0
我的线程开始啦
主线程:1
我的线程:100
我的线程:101
我的线程:102
主线程:2
我的线程:103
主线程:3
我的线程:104
我的线程:105
我的线程:106
主线程:4
主线程:5
我的线程:107
主线程:6
我的线程:108
主线程:7
主线程:8
主线程:9
主线程:10
主线程:11
主线程:12
主线程:13
主线程:14
我的线程:109
主线程:15
我的线程:110
我的线程:111
我的线程:112
我的线程:113
我的线程:114
我的线程:115
主线程:25
...
3.2.2、线程启动方式之Runnable接口(推荐使用,灵活度高,因为允许子类继承其他父类)
使用的构造方法:
-
public Thread(Runnable target)
-
public Thread(Runnable target, String name)
参数中的Runnable是一个接口,用来定义线程要执行的任务
线程启动步骤:
- 定义任务类实现Runnable,并重写run方法
- 创建任务对象
- 使用含有Runnable参数的构造方法,创建线程对象并指定任务
- 调用线程start方法,开启线程
示例:
任务类
public class MyTask implements Runnable{
@Override
public void run() {
for (int i = 100; i < 200; i++) {
System.out.println("新线程:"+ i);
}
}
}
启动线程
public class ThreadDemo1 {
public static void main(String[] args) {
MyTask myTask = new MyTask();
Thread t = new Thread(myTask);
t.start();
for (int i = 0; i < 100; i++) {
System.out.println("主线程:" + i);
}
}
}
运行结果与上面相似
4、线程中的常用方法
方法 | 说明 |
---|---|
String getName() | 获取线程名字 |
viod setName() | 给线程设置一个名字 |
public static Thread currentThread() | 返回当前正在执行的线程对象的引用 |
public static void sleep(long time) | 让线程休眠指定的时间,单位为毫秒 |
public void join() | 具备阻塞作用,等待这个线程死亡,才会执行其他线程 |
获取线程名字示例:
public class ThreadTask implements Runnable{
@Override
public void run() {
for (int i = 0; i < 30; i++) {
//获取 当前线程 的名字
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
public class Test1 {
public static void main(String[] args) {
new Thread(new ThreadTask()).start();
for (int i = 100; i <120 ; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
运行结果(部分展示):
main: 100
Thread-0: 0
main: 101
Thread-0: 1
main: 102
Thread-0: 2
main: 103
Thread-0: 3
main: 104
Thread-0: 4
main: 105
其他方法,大家可以自行尝试。
5、线程的安全问题
5.1、发生安全问题的原因
多个线程对同一个数据,进行读写操作,造成数据错乱
案例分析:我们现在设计一个卖票程序,票数总共100张,设置三个售票处
思路:
- 定义一个类Ticket实现Runnable接口,里面定义一个成员变量Private int count = 100
- 在Ticket类中重写run()方法实现卖票,代码如下
Ticket类
public class Ticket implements Runnable{
private int count = 100;
@Override
public void run() {
while (true){
if(count > 0){
//模拟出票时间延迟
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "票号:" + count);
count--;
}
if(count == 0){
break;
}
}
}
}
Test类
public class Test {
public static void main(String[] args) {
Ticket ticket = new Ticket();
//三个售票处
Thread t1 = new Thread(ticket, "售票处1-");
Thread t2 = new Thread(ticket, "售票处2-");
Thread t3 = new Thread(ticket, "售票处3-");
t1.start();
t2.start();
t3.start();
}
}
当我们运行后,发现结果出现了问题,有些情况不在我们的预期之中,不符合常理
比如出现了重复票:
还有出现了0号票:
为什么会出现这些奇怪的问题呢?很简单,这是因为多线程操作共享数据。解决这些问题的基本思路就是让共享数据存在安全环境中,当某一个线程访问共享数据时,其他线程是无法操作的。
具体实现:
- 把多条线程操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可
- java提供了同步代码块的实现方式来解决
5.2、同步代码块
5.2.1、线程同步
java允许多线程并发执行,当多个线程同时操作一个可共享的资源变量时(比如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁,以避免在该线程没有完成操作之前,被其它线程调用,从而保证该变量的唯一性和准确性。
5.2.2、同步代码块
锁住多条语句操作共享数据,可以使用同步代码块实现
- 格式:
synchronized(任意对象){
多条件语句操作共享数据的代码
}
- 默认情况锁是打开的,只要有一个线程进去执行代码了,锁就会关闭
- 当线程执行完成出来后,锁才会自动打开
- 锁对象是任意对象,但是多个线程必须使用同一把锁
同步的好处和弊端:
- 好处:解决了多线程的数据安全问题
- 弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
所以上面Ticket类中可以修改为:
public class Ticket implements Runnable{
private int count = 100;
Object lock = new Object();
@Override
public void run() {
while (true){
synchronized (lock){//锁对象可以时任意对象
if(count > 0){
//模拟出票时间延迟
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "票号:" + count);
count--;
}
if(count <= 0){
break;
}
}
}
}
}
5.2.3、同步方法
**概念:**就是把synchronized关键字加到方法上,保证线程执行该方法的时候,其他线程只能在方法外等着
- 格式
修饰符 synchronized 返回值类型 方法名(方法参数){
}
-
同步代码块和同步方法的区别
同步代码块可以锁住指定代码,同步方法是锁住方法中的所有代码
同步代码块可以指定锁对象,同步方法不能指定锁对象
虽然同步方法不能指定锁对象,但是有默认存在的锁对象
对于非static方法,同步锁就是this
对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。Class类型的对象
public class Ticket implements Runnable{
private int count = 100;
@Override
public void run() {
while (true){
if(count <= 0){
break;
}
demo();
}
}
private synchronized void demo(){
if(count > 0){
//模拟出票时间延迟
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "票号:" + count);
count--;
}
}
}
6、Lock锁机制
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了它,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。
Lock中提供了获得锁和释放锁的方法
- void lock():获得锁
- void unlock():释放锁
Lock是接口不能实例化,这里采用它的实现类ReentrantLock来实例化,ReentrantLock的构造方法是:ReentrantLock()。
注意:多个线程使用相同的Lock锁对象,需要多线程操作数据的代码放在lock()和unlock()方法之间。一定要确保最后unlock能够调用。
上述Ticket类可以修改为这样:
public class Ticket implements Runnable{
private int count = 100;
//获取锁对象
Lock lock = new ReentrantLock();
@Override
public void run() {
while (true){
//上锁
lock.lock();
if(count <= 0){
break;
}
if(count > 0){
//模拟出票时间延迟
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "票号:" + count);
count--;
}
//释放锁
lock.unlock();
}
}
}
7、线程通讯
7.1、概念
在多线程程序中,某个线程进入到等待状态时,必须有其他线程来唤醒处于等待状态的线程。
7.2、线程通讯需要使用的API
7.2.1、等待方法
- wait():无线等待(只能由其他线程唤醒)
- wait(long 毫秒):计时等待,时间到了自动唤醒
以上两个方法调用会导致当前线程释放掉锁资源
7.2.3、唤醒方法
- notify():唤醒处于等待状态的任意个线程
- notityAll():唤醒处于等待状态的所有线程
以上两个方法调用不会导致当前线程释放掉锁资源
无限等待唤醒示例:
public class Test {
public static void main(String[] args) {
Runnable task = new Runnable() {
Object lock = new Object();
boolean flag = true;
@Override
public void run() {
synchronized (lock){
if(flag){
flag = false;
System.out.println("线程进入无限等待...");
try {
lock.wait();
System.out.println("程序继续执行啦~");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else{
System.out.println("线程即将被唤醒");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
lock.notify();
}
}
}
};
new Thread(task).start();
new Thread(task).start();
}
}
计时等待唤醒示例:
public class Test01 {
public static void main(String[] args) {
new Thread(() -> {
Object lock = new Object();
synchronized (lock){
try {
System.out.println("即将进入计时等待...");
lock.wait(2000);
System.out.println("两秒了,程序继续执行");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
}
}
总结:
- 等待和唤醒方法调用需要使用对象锁,需要在同步代码中完成
- wait()进入无限等待,wait(时间)进入计时等待
- 唤醒线程的方法是notify【唤醒处于和notify使用同一对象锁上的,处于等待状态的任意线程】、notifyAll【同notify】
最后给大家来一个经典的生产者消费者简单线程代码示例:
公共资源类
public class Food {
//判断是否有食物
public static boolean food = true;
//锁
public static final Object lock = new Object();
}
生产者
public class Producer implements Runnable{
@Override
public void run() {
synchronized (Food.lock){
while(true){
//判断是否有食物,有食物则进行等待,反之则生产食物
if(Food.food){
//将食物标识设置为无
//Food.food = false;
System.out.println(Thread.currentThread().getName() + "此时进行等待消费者消费~");
try {
Food.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else{
System.out.println(Thread.currentThread().getName() + "发现没有食物,开始生产~");
//制作食物的时间
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
Food.food = true;
//唤醒消费者
Food.lock.notify();
}
}
}
}
public Producer() {
}
}
消费者
public class Consumer implements Runnable{
@Override
public void run() {
//判断是否有食物,有则消费,没有则等待
synchronized (Food.lock){
while(true){
if(Food.food){
System.out.println(Thread.currentThread().getName() + "发现食物,开炫~");
//消费时长
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
Food.food = false;
//唤醒生产者继续生产
Food.lock.notify();
}else{
System.out.println(Thread.currentThread().getName() + "没有发现食物,叫厨师~");
try {
Food.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
public Consumer() {
}
}
测试类
public class Test {
public static void main(String[] args) {
new Thread(new Producer(), "生产者").start();
new Thread(new Consumer(), "消费者").start();
}
}
部分运行结果
生产者此时进行等待消费者消费~
消费者发现食物,开炫~
消费者没有发现食物,叫厨师~
生产者发现没有食物,开始生产~
生产者此时进行等待消费者消费~
消费者发现食物,开炫~
消费者没有发现食物,叫厨师~
......
8、线程池
8.1、概述
8.1.1、线程使用存在的问题
如果并发的线程数量很多,并且每个线程都是执行一个很短的任务就结束了,这样频繁的创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
如果大量线程在执行,会涉及到线程间上下文的切换,会极大的消耗CPU运算资源
8.1.2、线程池认识
其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源
8.1.3、线程池使用的大致流程
- 创建线程池,指定线程开启数量
- 提交任务给线程池,线程池中的线程就会获取任务,进行任务处理
- 线程处理完任务不会销毁,而是返回到线程池中,等待下一个任务执行
- 如果线程池中所有的线程都被占用,提交任务,只能等待线程池中的线程处理完当前任务
8.1.4、线程池的好处
- 降低资源消耗:减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可以执行多个任务。
- 提高响应速度:当任务到达时,任务可以不需要等待线程创建,就能立即执行
- 提高线程的可管理性:可以根据系统的承受能力,调整线程池中工作线线程池的数目,防止因为消耗过多内存,服务器死机。
8.2、线程池中的API
8.2.1、线程池API学习
java.util.concurrent.ExecutorService是线程池接口类型,使用时我们不需要自己实现,JDK已经帮我们实现好了。获取线程池我们使用工具类java.util.concurrent.Executor的静态方法。
public static ExecutorService newFixedThreadPool(int num)指定线程池最大线程池数量获取线程池
线程池ExcutorService的相关方法:
提交执行的任务方法:
< T>Future< T>submit(Callable< T> task)
Future< ?> submit(Runnable task)
关闭线程池方法:
void shutdown() 启动一次顺序关闭,执行以前提交任务,但不接受新任务
示例:有三个老师要对五个学生进行一对一辅导:
public class Pool implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "在辅导学生学习");
}
}
public class Test {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(3);
pool.submit(new Pool());
pool.submit(new Pool());
pool.submit(new Pool());
pool.submit(new Pool());
pool.submit(new Pool());
}
}
测试结果:
pool-1-thread-2在辅导学生学习
pool-1-thread-1在辅导学生学习
pool-1-thread-3在辅导学生学习
pool-1-thread-1在辅导学生学习
pool-1-thread-2在辅导学生学习
8.3、Callable接口
8.3.1、概述
public interface Callable<V> {
V call() throws Exception;
}
Callable和Runnable的不同点:
- Callable支持返回结果,Runnable不行
- Callable支持抛出异常,Runnable不行
8.3.2、使用步骤
- 创建线程池
- 定义Callable任务
- 创建Callable任务,提交任务给线程池
- 获取执行结果
利用线程池计算0-n的和并返回结果:
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService demo = Executors.newFixedThreadPool(10);
Callable<Integer> task = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 10; i++) {
sum += i;
}
return sum;
}
};
Future<Integer> future = demo.submit(task);
System.out.println(future.get());
}
}
ble和Runnable的不同点:**
- Callable支持返回结果,Runnable不行
- Callable支持抛出异常,Runnable不行
8.3.2、使用步骤
- 创建线程池
- 定义Callable任务
- 创建Callable任务,提交任务给线程池
- 获取执行结果
利用线程池计算0-n的和并返回结果:~
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService demo = Executors.newFixedThreadPool(10);
Callable<Integer> task = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 10; i++) {
sum += i;
}
return sum;
}
};
Future<Integer> future = demo.submit(task);
System.out.println(future.get());
}
}
呼~~~