线程
1. 线程
定义
:线程是进程的组成部分,不同的线程执行不同的任务,不同的功能模块,同时线程使用的资源师由进程管理,主要分配CPU和内存。 在进程中,线程执行的方式是抢占式执行操作,需要考虑的是当前的线程是否持有CPU的执行权和等待权,同时关注线程的状态。
多线程
:有了多线程,我们就可以让程序同时处理多个事件。作用能够快速提高效率。
并发和并行的区别
并发:表示的是在同一时刻,多个任务在单个的CPU上的交替执行的操作。
并行:表示的是在单位时间内,多个处理器或者多核处理器的同时处理的方式,是真正意义上的同时进行。
串行:表示的是有N个任务,通过一个线程进行顺序的执行操作,由于任务中只存在一个任务,方法都是在一个线程的执行的,所以不存在线程的不安全的情况,也就是不存在临界区的问题。比如两个人排队使用一台电脑。
1.1 多线程的优点
有利于提高CPU的利用效率,允许单个程序创建多个并行执行的线程来完成各自的任务
1.2 多线程的缺点
- 线程也是程序,所以线程是需要进行内存的占用的,线程越多,内存的占用越多
- 多线程需要进行协调和管理,所以需要CPU时间跟踪线程
- 线程之间对于共享资源的访问是相互影响的,必须解决竞用共享资源的问题。
1.3 守护线程
守护线程是运行在后台的,为其他的前台的线程提供服务的,也可以说守护线程是JVM中非守护线程的“佣人”一旦所有的线程都结束运行,守护线程会随着JVM进行运行操作。
2. 线程的创建
2.1 通过继承Thread类的方式
Tread类是线程类,一种反方是将类声明为Thread的子类的方式,该子类的应该通过重写run方法的方式来创建线程。
使用的步骤格式:
- 自己定义一个类继承Thread
- 重写run方法
- 创建子类对象,而且进行线程的启动
class MyThread1 extends Thread {
@Override
public void run() {
// 线程代码
}
}
main() {
// 实例化自定义继承 Thread 类子类对象
MyThread1 mt1 = new MyThread1();
// 利用继承父类 Thread 中的 start 方法,启动线程。【切记不可以直接调用 run 方法】
mt1.start();
}
【重点】
为什么我们通过调用start方法来执行run方法,而不是直接调用run方法
- 首先new一个Tread线程,线程进入到新创建的状态中,调用start方法,会启动一个线程并且将线程进入到就绪的状态,当分配到时间片的时候,就能够进行工作了。start() 方法会执行线程的准备工作,然后自己进行执行run() 方法的内容,这才是真正的多线程的工作。
- 直接执行run方法的时候,main方法会将其看做一个普通的反方进行执行操作,并不会在某个线程中去执行它,所以不是多线程的工作。
2.2 通过实现Runnable接口
多线程的创建的步骤:
- 自己定义一个类实现Runnable接口
- 重写里面的run方法
- 创建自己的类对象
- 创建一个Tread类的对象,Runnable作为参数传入,并且开启线程
Runnable 接口是一种函数式接口,@FuncationalInterface ,能够使用lambda表达式。
class MyThread2 implements Runnable {
@Override
public void run() {
// 线程代码
}
}
main() {
/*
可以利用 Thread 类构造方法,实例化目标线程对象,执行对应线程目标
将 Runnable 接口的实现类对象作为 Thread 构造方法参数
构造方法 Thread(Runnable target);
构造方法的作用是以 :Runnable 接口实现类为当前线程的执行目标,new 关键字 + 构造方法实例化线程对象
*/
Thread t1 = new Thread(new MyThread2());
t1.start();
}
真实案例的代码实现:
class MyThread1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("自定义线程继承 Thread 类型完成线程类");
}
}
}
class MyThread2 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("自定义线程遵从 Runnable 接口实现线程类");
}
}
}
/**
* Thread 自定义线程类实现的两种方式
*
* @author Anonymous 2023/3/8 10:06
*/
public class Demo1 {
public static void main(String[] args) {
MyThread1 mt1 = new MyThread1();
Thread mt2 = new Thread(new MyThread2());
mt1.start();
mt2.start();
for (int i = 0; i < 100; i++) {
System.out.println("main方法内容...");
}
}
}
2.3 通过实现Callable接口(有返回结果)
多线程的第三种的实现的方式:
- 创建一个 自定义 类实现java.util.concurrent包Callable接口
- 重写call方法(
有返回值
,表示的是多线程的运行的结果)- 创建MyCallable的对象(表示多线程要进行执行的任务)
- 创建FutureTask的对象(作用是管理多线程的运行的结果)
- 创建Thread类的对象,并且进行启动的操作(表示线程)
【注意】:其中的callable是存在泛型的格式的,其中的泛型表示的是返回值的泛型
//方法步骤
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
/*
* 用来求解1~100 的和
*/
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum += i;
}
return sum;
}
}
// 测试实现操作
public class TestMain {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/*
* 1. 创建MyCallable的对象(表示的是多线程要执行的任务)
*/
MyCallable myCallable = new MyCallable();
/*
2. 创建FutureTask的对象,作用是用来管理多线程的运行的结果
*/
FutureTask<Integer> integerFutureTask = new FutureTask<>(myCallable);
/*
* 3. 创建线程的对象
*/
Thread thread = new Thread(integerFutureTask);
/*
4.启动线程
*/
thread.start();
/*
* 获取多线程的运行的结果
*/
Integer integer = integerFutureTask.get();
System.out.println(integer);
}
}
总结
- Tread 继承的操作,编码比较简单,可以直接使用Tread类中的方法,可以拓展的可能性比较差,不能够继承其他的类
- 接口的方式是可拓展性是比较强的,实现接口的同时还可以继承其他的类,编程相对来说比较复杂,不能够直接使用Tread类中方法。
2.4 线程中操作方法
方法名称 | 作用 |
---|---|
String getName() | 获取指定线程对象的名称 |
void setName(String name) | 针对于线程的名称获取和设置 |
int getPriority() | 获取指定线程的优先级 |
void setPriority(int newPriority) | 获取指定线程的优先级 默认是5 只能够提供被执行的概率,不能确保一定会执行操作 |
final void setDaemon(boolean flag) | 设置指定线程为守护线程 |
boolean isDaemon() | 设置和判断当前线程是否为守护线程 |
static void sleep(long time)单位是ms | 强迫一个线程睡眠的时间,单位是毫秒级别的 |
static Thread currentThread(); | 在哪一个线程代码中执行,获取当前线程代码对应的线程对象。 |
static void yield() | 出让线程,礼让线程的操作 |
static void join() | 插入线程、插队线程的操作 |
构造方法:
Thread();
过
Thread(Runnable target);
以 Runnable 接口实现类为当前线程的执行目标,new 关键字 + 构造方法实例化线程对象
Thread(Runnable target, String threadName);
以 Runnable 接口实现类为当前线程的执行目标,同时明确当前线程的名字,
new 关键字 + 构造方法实例化线程对象
对于指定的方法进行分析的操作
1. String getName() : 表示的是获取指定的线程的名称,如果没有设置名称,线程也会自动生成名称的,通过源码我们能够看到是this当前线程 + nextThreadNum(): 是一个变量的自动递增。
2. void setName(String name):对于线程进行名字的设置除了调用方法的设置的方式之外,还能够通过调用构造方法的方式来进行实现:比如说Thread(String threadName) 以及 Thread(Runnable target, String threadName)两种方法来实现线程名称的命名。
3.static Tread currentThread() :获取当前线程的对象。细节是:当JVM虚拟机进行启动的时候,会自动的启动多条线程,其中有一条线程是main线程,它的作用是调用main方法,并且执行里面的代码,在以前的时候,我们所写的代码其实都是运行在main线程中
4.static void sleep(long time):那条线程执行到这个反方,那么那条线程就会在这里进行停留时间。方法的参数,就表示的是睡眠的时间,时间的单位毫秒。1s = 1000ms 当时间到了之后,线程会自动的醒来,继续执行下面的其他的代码。
5.final void setDaemon(boolean flag) 当其他的非守护线程执行完毕之后,守护线程会陆续的结束,既当其他线程结束操作之后,守护线程也就没有存在的必要了。
/**
* 通过实现runnable接口实现线程的创建
*/
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"____"+i);
}
}
}
public class Demo1 {
public static void main(String[] args) {
/*
* 创建线程需要执行的参数对象
*/
Runnable runnable = new MyRunnable();
/*
* 创建线程对象
*/
Thread thread = new Thread(runnable, "张三");
Thread thread1 = new Thread(runnable, "李四");
/*
* 通过更改优先级的大小,提高线程优先运行的概率,不能够保证一定是优先执行的操作。
*/
thread.setPriority(10);
thread1.setPriority(6);
/*
* 进行线程的执行的操作,需要进行开启线程操作
*/
thread.start();
thread1.start();
}
}
运行的结果:
李四____0
张三____0
张三____1
张三____2
张三____3
张三____4
张三____5
张三____6
张三____7
张三____8
张三____9
李四____1
李四____2
李四____3
李四____4
李四____5
李四____6
李四____7
李四____8
李四____9
2.5 线程的生命周期
sleep 方法会让线程睡眠,睡眠的时间到了之后,不会立刻执行下面的代码:
不会
直接执行,会进入到就绪的状态上,然后进行线程资源的抢夺,如果抢夺到资源后能够运行。
3. 线程安全
线程安全的问题:对于线程安全来说,所有的隐患都是出现多个线程访问的情况下产生的,我们能够确保在多条线程访问的情况下,我们的程序能够按照我们预期的行为去执行。
需求:某电影院中目前需要上映国产的大片,共有100张票,需要三个窗口进行售票的行为,请设计一个程序模拟电影院进行买票的行为。
// 采用继承的方式创建对象
/**
* 通过继承的方式操作线程
*/
public class MyThread extends Thread{
/*表示的是这个类中的所有的对象,都在共享tickets的数据
*/
static int ticket = 0;
@Override
public void run() {
while (true) {
if (ticket < 100){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket ++ ;
System.out.println(getName()+"正在卖"+ticket+"张票");
} else {
break;
}
}
}
}
// 进行检验
public class Demo1 {
/**
*检验多线程的问题
*
* @param args
*/
public static void main(String[] args) {
/*
* 1. 创建线程对象
*/
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
MyThread myThread3 = new MyThread();
/*
通过调用方法起名字
*/
myThread1.setName("窗口1");
myThread2.setName("窗口2");
myThread3.setName("窗口3");
/*
开启线程的操作
*/
myThread1.start();
myThread2.start();
myThread3.start();
}
}
运行的结果:
......
窗口1正在卖96张票
窗口3正在卖97张票
窗口1正在卖99张票
窗口2正在卖99张票
窗口3正在卖100张票
窗口1正在卖101张票
窗口2正在卖102张票
分析:出现重复的数据卖出,同时出现数据的溢出操作
总结:在进行循环的时候,出现线程满足循环的条件,所以进行执行的操作,出现线程争夺资源的情况,所以造成的问题是关于线程的安全的问题。
3.1 同步代码块
为了解决上述的问题的情况,我们可以引入的方法是同步的代码块的操作,把操作共享数据的资源进行加锁的操作
特点
- 锁默认的是打开的关系,有一个线程进去,锁自动关闭
- 里面的代码全部执行完毕,线程出来,锁自动打开。
格式:
synchronized(锁){
操作共享数据的代码
}
// 改进的代码操作如下所示:
public class MyThread extends Thread{
/*表示的是这个类中的所有的对象,都在共享tickets的数据
*/
static int ticket = 0;
/**
*
针对于第一种的创建的方式,可能出现创建多种的线程对象,因此将之加上static 将数据共享 ,如果是对于方法二而言:创建的只有一种Runnable对象。而且是当做参数,创建线程的操作。因此不需要进行共享的声明。
*/
@Override
public void run() {
while (true) {
//此处是更改之后的数据的操作
synchronized ("锁对象要求是唯一"){
if (ticket < 100){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket ++ ;
System.out.println(getName()+"正在卖"+ticket+"张票");
} else {
break;
}
}
}
}
}
改进后的运行的效果是:
......
窗口3正在卖95张票
窗口2正在卖96张票
窗口2正在卖97张票
窗口3正在卖98张票
窗口1正在卖99张票
窗口3正在卖100张票
【重点操作】:
在锁结构中,我们能够使用的过程中加上唯一的锁对象,对于锁对象的选择上来说
- 必须保证唯一的特性
- 通常是一个类内会设置一个独立的变量作为锁对象使用
- 包装类不得用于锁对象使用
- 当前类型的Class对象可以作为锁对象,但是不推荐
3.2 同步方法
使用的关键字是synchronized
限制对应的方法是有且只能通过一个线程进入到方法中执行目标的任务
重点进行区分的是有static修饰的方法是静态方法,需要进行考虑的是当前锁对象的情况,以及同步范围的情况。
同步方法的格式:
修饰符 synchronized 返回值类型 方法名称(方法参数){
}
1.同步方法是锁住方法中的所有的代码
2. 锁对象不能够自己指定
// 注意:非静态的同步方法对象是当前调用方法的对象 -> this
public synchronized void sale() {...}
// 静态同步方法锁对象是当前类.class 类名.class 唯一的锁对象的特征
public static synchronized void staticSale() {...}
3.2.1 同步方法的范围
非静态的成员方法使用的是 synchronized 同步的约束,同步的范围
重点
:通过当前的实例化对象作为锁对象,实例化对象调用任意 synchronized 修饰的同步方法,所有其他的 synchronized 同步方法,一律不能够进行执行的操作
java中经常使用的是:
ArrayList 和 Vector
StringBuilder 和 StringBuffer
HashMap 和 HashTable
静态的成员方法使用 synchronized 同步进行约束的操作,锁对象是 类名.class .无论那一条线程,任何一个对象来执行目标方法,都会被 类名.class 锁住,其他的任意的线程,任意的对象,都无法执行所有其他 static 修饰的 synchronized 约束的同步方法。
例子一:
public static synchronized void test1() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Test1 方法");
}
}
/*
等价于
synchronized (TypeB.class) {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Test1 方法");
}
}
*/
例子二:
电影院售票的操作套路
采用的是通过实现接口的方式来创建线程
public class MyRunnable implements Runnable{
int ticket = 0;
/**
* 执行的套路是:
* 1. 循环
* 2. 同步代码块
* 3.判断共享数据是否到了末尾,到末尾的情况
* 4. 判断共享数据是否到了末尾,没有到末尾的情况
*/
@Override
public void run() {
while (true) {
synchronized (MyRunnable.class) {
if (ticket == 100){
break;
} else {
ticket ++;
System.out.println(Thread.currentThread().getName()+"买了第"+ticket+"张票");
}
}
}
}
}
更改成为同步方法的格式为:
public class MyRunnable implements Runnable{
int ticket = 0;
/**
* 执行的套路是:
* 1. 循环
* 2. 同步代码块
* 3.判断共享数据是否到了末尾,到末尾的情况
* 4. 判断共享数据是否到了末尾,没有到末尾的情况
*/
@Override
public void run() {
while (true) {
if (extracted()) break;
}
}
/**
* 其中的锁对象是惟一的,其中的对象表示的是this,即为当前的对象
*
* @return
*/
private synchronized boolean extracted() {
if (ticket == 100){
return true;
} else {
ticket ++;
System.out.println(Thread.currentThread().getName()+"买了第"+ticket+"张票");
}
return false;
}
}
通过创建接口对象,并且将接口对象作为参数进行传入的操作
*/
public class TicketDemo {
public static void main(String[] args) {
/**
* 通过接口的实现,来进行操作,将接口名作为参数,创建线程
*/
Runnable runnable = new MyRunnable();
Thread thread0 = new Thread(runnable);
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread0.setName("窗口1");
thread1.setName("窗口2");
thread2.setName("窗口3");
thread0.start();
thread1.start();
thread2.start();
}
}
3.3 Lock锁
- Lock 实现提供的比 synchronized 方法和语句可以获取更为广泛的锁定操作
- Lock中提供了获的锁和释放锁的方法
- void lock() 获得锁
- void unlock() 释放锁
- lock接口是不能够进行直接的实例化操作的,这里采用的它的实现类ReentrantLock 来实例化 ReentrantLock的构造方法。类ReentrantLock() 创建一个类ReentrantLock的实例。
总结
: 手动上锁,手释放锁。
手动锁的结构是:
static Lock mylock = new ReentrantLock();
mylock.lock;
try{
}finally{
mylcok.unlock();
// 如果在临界区的代码抛出一个异常,锁必须释放。否则其他的线程将永远阻塞。
}
这个结构的优点是确保任何时刻,都只有一个线程进入临界区,一旦一个线程锁定了锁对象,其他的任何线程都无法通过lock语句,当其他的语句调用lock语句的时候,他们会暂停,直到第一个线程释放这个锁对象。
出现的问题:
1. 创建三个线程对象,lock对象的创建是在类里面
2. 所以出现的是三种不同的锁,所以有可能出现的是没有加锁成功
3. 改正的方法可以在前面加上 static 修饰符
public class MyThread extends Thread{
/*表示的是这个类中的所有的对象,都在共享tickets的数据
*/
static int ticket = 0;
Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
/*
调用获得锁的方法操作,开始上锁
*/
lock.lock();
if (ticket < 100){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket ++ ;
System.out.println(getName()+"正在卖"+ticket+"张票");
} else {
break;
}
// 关锁的操作
lock.unlock();
}
}
}
public class Demo1 {
/**
*检验多线程的问题
*
* @param args
*/
public static void main(String[] args) {
/*
* 1. 创建线程对象
*/
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
MyThread myThread3 = new MyThread();
/*
通过调用方法起名字
*/
myThread1.setName("窗口1");
myThread2.setName("窗口2");
myThread3.setName("窗口3");
/*
开启线程的操作
*/
myThread1.start();
myThread2.start();
myThread3.start();
}
}
改进之后的作用:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 通过继承的方式操作线程
*/
public class MyThread extends Thread{
/*表示的是这个类中的所有的对象,都在共享tickets的数据
*/
static int ticket = 0;
static Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
/*
调用获得锁的方法操作,开始上锁
*/
lock.lock();
try {
if (ticket < 100){
Thread.sleep(100);
ticket ++ ;
System.out.println(getName()+"正在卖"+ticket+"张票");
} else {
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 关锁的操作
lock.unlock();
}
}
}
}
3.4 死锁(错误)
死锁实际上是:指的是两个或者两个以上的进程在执行的过程中,由于竞争资源或者由于彼此的铜线而造成的一种阻塞的现象。若没有外力的作用,它们无法推进下去。
3.4.1 死锁的四个必要的条件
- 互斥条件:在一定的时间某种资源只有一个线程占用。如果此时还有其他的进程请求资源,就只能够等待,直到占有资源的进程用完释放
- 占有且等待条件:指进程已经保持至少一个资源,但是又提出了新的资源的请求,而且该资源已经被其他进程占用,此时请求进程阻塞,但是对于自己已经获得的其他的资源保持不放
- 不可抢占资源:别人已经占有额某项资源,你不能够因为自己也需要该资源,就去把别人的资源抢过来。
- 循环等待的条件:若干个进程之间形成一种头尾相接的循环等待的资源关系。
3.4.2 如何避免死锁
- 避免一个线程同时获得多个锁
- 避免一个线程在锁内同时占用多个资源,尽量保持每个锁只占用一种资源
- 尝试使用定时锁,使用lock.tryLock(timeout)来代替使用内部锁机制
3.5 线程安全
- 临界资源:共享资源(同一个对象),一次只可以有一个线程操作,才能够保证准确性
- 原子操作:不可拆分的步骤,被视为一个整体,其中的步骤不能够被打乱。
3.6 线程不安全
- 完整的步骤可能会被破坏
- 线程内的数据可能会被别的线程修改
举例
public class Printer extends Thread{ // 锁需要是同一对象 private Object obj = new Object(); // 打印机1号 public synchronized void print1() { //synchronized (obj){ System.out.print(1+" "); System.out.print(2+" "); System.out.print(3+" "); System.out.print(4+" "); System.out.print("\r\n"); //} } // 打印机2号 public synchronized void print2() { //synchronized (obj) { System.out.print("一 "); System.out.print("二 "); System.out.print("三 "); System.out.print("四 "); System.out.print("\r\n"); //} } }
public class Demo1 { public static void main(String[] args) { Printer printer = new Printer( ); // 线程1 new Thread(() -> { while(true){ printer.print1(); } }).start(); // 线程2 new Thread(() -> { while(true){ printer.print2(); } }).start(); } }