文章目录
七、多线程
1. 什么是进程?什么是线程?
进程是一个应用程序(1个进程是一个软件)
线程是一个进程中的执行场景 / 执行单元。
一个进程可以启动多个线程。
对于java程序来说,当在DOS命令窗口中输入:
java HelloWorld 回车后,会先启动JVM,而JVM就是一个进程。
JVM再启动一个主线程调用main方法。
同时再启动一个垃圾回收站负责看护,回收垃圾。
最起码, 现在的java程序中至少有两个线程并发,
一个是垃圾回收线程,一个是执行main方法的主线程。
线程A和线程B:
堆内存和方法区内存共享,但是栈内存独立,一个线程一个栈。
假如有10个线程,会有10个栈空间,每个栈和每个栈之间
互不干扰,各自执行各自的,这就是多线程并发。
java中之所以有多线程机制,目的是为了提高程序的执行效率。
一个线程一个栈:
java语言中实现线程有两种方式
java支持多线程机制,并且java已经将多线程实现了,只需要继承就可以了。
第一种方式: 编写一个类,直接继承java.lang.Thread ,重写run方法。
第二种方式: 编写一个类,实现java.lang.Runnable接口,实现run方法。
注意:第二种方式比较常用,因为一个类实现了接口,它还可以去继承其他的类,更灵活。
2. 线程的生命周期:
新建状态–> 就绪状态–>运行状态(–>阻塞状态–>就绪状态 )–>死亡状态
获取线程的对象:
Thread t = Thread.currentThread();
返回值 t 就是当前线程对象。
设置线程的名字: 线程对象.setName("线程名字");
获取线程的名字: 线程对象.getName();
3. 线程的Sleep()方法
Static void sleep(long millis)
- 静态方法 :Thread.sleep(1000)
- 参数是毫秒
- 作用:让当前线程进入休眠,进入”阻塞状态“,放弃占有CPU时间片,让给其他线程使用
这行代码出现在A线程中,A线程就会进入休眠。
这行代码出现在B线程中,B线程就会进入休眠。
线程名.interrupt() : 睡眠终断 (终断t线程的睡眠:这种终断睡眠的方式依靠了Java的异常处理机制)
强行终止线程的进行:
public class ThreadSleepTest04 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable3());
t.setName("t");
t.start();
// 模拟5秒
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 5秒之后强行终止t线程
t.stop(); // 已过时,不建议使用
}
}
class MyRunnable3 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
合理的终止进程方式:
public class ThreadSleepTest05 {
public static void main(String[] args) {
myRunnable5 r5 = new myRunnable5();
Thread t = new Thread(r5);
t.setName("t");
t.start();
// 模拟5秒
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
// e.printStackTrace();
}
// 终止线程
// r5.run = false;
}
}
class myRunnable5 implements Runnable {
// 打印一个布尔印记
boolean run = true;
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (run) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
// 终止当前进程
// 在结束前可以保存数据
// save......
return;
}
}
}
}
让位 :让当前线程暂停,回到就绪状态,让给其他线程。
静态方法:Thread.yield();
合并: join()方法
如:分支线程合并到主线程中,主线程受阻碍,分支线程执行直到结束,再继续执行主线程。
4. 线程安全
4.1 synchronized同步机制(锁)
什么环境下会存在安全问题?
怎么解决线程安全问题?
使用 ” 线程同步机制 “
线程同步,有两个编程模型:
线程同步机制的语法是:
synchronized (多线程共享的对象){
// 线程同步代码块
}
synchronized后买你小括号中传的”数据“是相当关键的
这个数据必须是多线程共享的数据,才能达到多线程排队。
在java语言中,任何一个对象都有”一把锁“其实这把锁就是标记。(只是把它叫做锁)
100个对象,100把锁。1个对象1把锁。
运行过程:
进入锁池,可以理解为是一种阻塞状态
在实例方法上使用synchronizatized
synchronizatized使用在实例方法上有什么优点?
代码写的少乐,简约代码。
如果共享的对象就是this,并且需要同步的代码块是整个方法体,建议使用这种方式。
总结synchronizatized的写法:
4.2 死锁
synchronizatized在开发中最好不要嵌套使用,否则可能会导致死锁现象的发生。
死锁的代码例子:
public class deadlock01 {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
Thread t1 = new MyThread1(o1, o2);
Thread t2 = new MyThread2(o1, o2);
t1.start();
t2.start();
}
}
class MyThread1 extends Thread {
Object o1;
Object o2;
public MyThread1(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
public void run() {
synchronized (o1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
}
}
}
}
class MyThread2 extends Thread {
Object o1;
Object o2;
public MyThread2(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
public void run() {
synchronized (o2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1) {
}
}
}
}
5. 开发中怎么解决线程安全问题
6. 守护线程
6.1 守护线程的介绍
java语言中线程分为两大类:
一类是:用户线程
一类是: 守护线程(后台线程)
其中具有代表性的就是:垃圾回收线程(守护线程)。
6.2 守护线程的特点:
一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。
注意: 主线程main方法是一个用户线程。
守护线程用在什么地方?
实现守护线程:
线程名.setDaemon(true);
public class ThreadTest07 {
public static void main(String[] args) {
Thread t = new BakDataThread();
t.setName("备份数据的线程");
// 启动线程前,将线程设置为守护线程。
t.setDaemon(true);
t.start();
// 主线程:主线程是用户线程
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "------>" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class BakDataThread extends Thread {
// 即使是死循环,但由于该线程是守护者,当用户线程结束,守护线程自动终止。
public void run() {
int i = 0;
while (true) {
System.out.println(Thread.currentThread().getName() + "----->" + (++i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果:
当主线程(用户线程)结束后,即使线程是死循环,也会自动终止线程。
7. 定时器
定时器的作用:
间隔特定的时间,执行特定的程序。
每周要进行银行账户的总帐操作。
每天要进行数据的备份操作。
在实际的开发中,每隔多久执行一段特定的程序,这种需求是很常见的,
那么在java中其实可以采用多种方式实现:
可以使用sleep方法,睡眠,设置睡眠时间,没到这个时间点醒来,执行
任务。这种方式是最原始的定时器。(比较low)
在java的类库中已经写好了一个定时器:java.util.Timer ,可以直接拿来用。
不过,这种方式在目前的开发中也很少用,因为现在有很多高级框架都是支持定时任务的。
在实际的开发中,目前使用的较多的是spring框架中提供的springTask框架,这个框架
只要进行简单的配置,就可以完成定时的任务。
实现定时器:
public class TimerTest {
public static void main(String[] args) throws Exception{
// 创建定时器对象
Timer timer = new Timer();
// Timer timer = new Timer(true); 守护线程的方式
// 指定定时任务
// time.schedule(定时任务,第一次执行时间,间隔多少执行一次)
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date fisrtTime =sdf.parse("2023-01-07 11:25:30");
timer.schedule(new LogTimerTask(),fisrtTime,1000*10);
}
}
// 编写一个定时任务类
//假设这是一个记录日志的定时任务
class LogTimerTask extends TimerTask{
@Override
public void run() {
// 编写执行的任务
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd:mm:ss");
String strTime = sdf.format(new Date());
System.out.println(strTime+ "完成了一次数据备份!");
}
}
运行结果:
每隔10秒自动进行数据备份
8. 实现线程的第三种方式:FutureTask方式,实现callable接口(JDK8新特性)
实现线程的第三种方式: 实现callable接口
这种方式的优点:可以获取到线程的执行结果
这种方式的缺点:效率比较低,在获取t线程执行结果的时候,当前线程阻塞,效率较低。
public class ThreadTest08 {
public static void main(String[] args) {
// 第一步: 创建一个“未来任务类” 对象
// 参数非常重要,需要给一个callable接口实现类对象.
FutureTask task = new FutureTask(new Callable() {
@Override
public Object call() throws Exception {
// call() 方法就相当于run方法。只不过这个有返回值
// 线程模拟一个任务,执行之后可能会有一个执行官结果
// 模拟执行
System.out.println("call method begin!");
Thread.sleep(1000 * 10);
System.out.println("call method over!");
int a = 100;
int b = 200;
return a + b; // 自动装箱(300结果变成个Interger)
}
});
// 创建线程对象
Thread t = new Thread(task);
// 启动线程
t.start();
// 这里是main方法,这是在主线程种
// 在主线程种,怎么获取t线程的返回结果?
// get()方法的执行会导致“当前线程阻塞”
try {
Object obj = task.get();
System.out.println("线程执行结果:" + obj);
} catch (InterruptedException e) {
// e.printStackTrace();
} catch (ExecutionException e) {
// e.printStackTrace();
}
/* main 方法这里的程序想要执行必须等待get()方法的结束
* 而get()方法可能需要很久,因为get()方法是为了拿另一个线程的执行结果
* 而另一个线程执行是需要时间的
* */
System.out.println(" hello world!");
}
}
运行结果:
9. Object类中的wait 和 notify方法。(生产者和消费者模式!)
用图表示:
生产者和消费者模式:
实现生产者于消费者模式:
例子:
public class ThreadTest09 {
public static void main(String[] args) {
// 创建一个仓库对象,共享的
List list = new ArrayList();
// 创建两个线程对象
// 生产者对象
Thread t1 = new Thread(new producer(list));
// 消费者对象
Thread t2 = new Thread(new consumer(list));
t1.setName("生产者线程");
t2.setName("消费者线程");
t1.start();
t2.start();
}
}
class producer implements Runnable {
// 仓库
private List list;
public producer(List list) {
this.list = list;
}
@Override
public void run() {
synchronized (list) {
while (true) {
synchronized (list) {
if (list.size() > 0) { // 大于0 ,说明仓库中已经有一个元素了
try {
// 当前线程进行等待状态,并且释放producer之前占有的list集合的锁
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序能执行到这里说明仓库是空的,可以生产
Object obj = new Object();
list.add(obj);
System.out.println(Thread.currentThread().getName() + "----->" + obj);
try {
Thread.sleep(1000 * 3);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 换醒消费者进行消费
list.notify();
}
}
}
}
}
class consumer implements Runnable {
private List list;
public consumer(List list) {
this.list = list;
}
@Override
public void run() {
while (true) {
synchronized (list) {
if (list.size() == 0) {
try {
// 仓库已经空了
// 消费者线程等待,释放掉list集合的锁
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序能执行到此处,说明仓库中有数据,进行消费.
Object obj = list.remove(0);
System.out.println(Thread.currentThread().getName() + "------->" + obj);
try {
Thread.sleep(1000 * 3);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 唤醒生产者生产
list.notify();
}
}
}
}
运行结果: