1)多线程的介绍
1.1、进程和线程的区别
- 进程是一个应用程序(一个进程是一个软件)
- 线程是一个进程中的执行场景/执行单元
一个进程可以启动多个线程
1.2、进程和线程的关系
进程可以比作是现实生活当中的中公司
线程可以比作是公司当中的某个员工
注意:
一、进程A和进程B的内存独立不共享
二、在Java语言中,线程A和线程B的堆内存和方法区内存共享。但是栈内存独立,一个线程一个栈。
三、当使用多线程机制后,main方法结束只是主线程结束,其它的栈(线程)可能还在运行。
1.3、线程异步/线程同步
异步编程模型:
线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,
谁也不需要等谁,这种编程模型叫做:异步编程模型。
其实就是:多线程并发(效率较高。)
同步编程模型:
线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行结束,
或者说在t2线程执行的时候,必须等待t1线程执行结束,
两个线程之间发生了等待关系,这就是同步编程模型。
效率较低。线程排队执行。
1.4、线程对象的生命周期
新建状态:new出线程对象
就绪状态:对象调用start()方法时进入就绪状态
运行状态:线程对象的run()方法开始执行 或者 继续执行时进入运行
阻塞状态:遇到阻塞事件进入阻塞
死亡状态:线程对象的run()方法执行完毕后进入死亡
2)多线程的实现
第一种方式:编写一个类,直接继承java.lang.Thread,重写run()方法
public static void main(String[] args){
//创建线程对象
MyThread t=new MyThread();
//t.run(); //不会启动线程,不会分配新的分支栈。(这种方式就是单线程。)
t.start();//启动线程,自动调run()方法
}
}
//定义线程类
class MyThread extends Thread {
@Override
//这段代码运行在分支线程中(分支栈)
public void run() {
for(int i=0;i<10;i++){
System.out.println("分支线程---->"+i);
}
}
}
第二种方式:编写一个类,实现java.lang.Runnable接口,实现run()方法。也可以采用匿名内部类方式
这种方式比较常用,因为一个类实现了接口,它还可以去继承其他类,更灵活
public static void main(String[] args){
//创建一个可运行对象
MyRunnable r=new MyRunnable();
//将可运行对象封装成一个线程对象
Thread t=new Thread(r);
//Thread t=new Thread(new MyRunnable22()); //合并代码
//启动线程
t.start();
}
}
这并不是一个线程类,是一个可运行的类。它还不是一个线程。
class MyRunnable implements Runnable{
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println("分支线程---->"+i);
}
}
}
第三种方式:编写一个类,实现java.util.concurrent.Callable接口,实现call()方法。也可以采用匿名内部类方式
优点:可以获取线程的执行结果
缺点:在获取某个线程执行结果时,当前线程受阻塞,效率较低。
public static void main(String[] args) {
//创建一个Callable对象
MyCallable c=new MyCallable();
//将Callable对象封装成“未来任务类”对象。
FutureTask task=new FutureTask(c);
//创建一个线程对像
Thread t=new Thread(task);
t.start();
try {
//get()方法可以获得t线程的返回结果
//但该方法的执行会导致当前线程的阻塞
Object o=task.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
//要等到get方法结束,才执行下面语句
System.out.println("当前线程(主线程)");
}
}
class MyCallable implements Callable{
@Override
// call()方法就相当于run方法。只不过这个有返回值
public Object call() throws Exception {
System.out.println("Call begin");
Thread.sleep(1000*10);
System.out.println("Call over");
int a=10,b=20;
return a+b; //自动装箱
}
}
3)多线程的常用方法
//1、获取当前线程对象
Thread t = Thread.currentThread();
//2、获取线程对象的名字
String name = 线程对象.getName();
//3、修改线程对象的名字
线程对象.setName("线程名字"); //默认名字为Thread-i (i表示数字)
// 4、让当前对象进入“阻塞状态”,放弃占有CPU时间
Thread.sleep(1000); //Thread的静态方法,参数是毫秒
注意:当用引用.sleep(1000)时,它会自动转成Thread.sleep(1000);
让当前线程进入休眠,是当前对象
//5、终断t线程的睡眠 (依靠Java的异常处理机制)
t.interrupt();
//6、让位。当前线程暂停,回到就绪状态,让给其他线程
Thread.yield(); //静态方法
注意:在回到就绪状态后,有可能再次抢到CPU时间片??
//7、线程合并
t.join(); //t线程合并到当前线程中,当前线程受阻塞,直到t线程执行结束
//8、守护线程
t.setDaemon(true); //将t线程设置为守护线程,当用户线程结束时,该线程也结束
守护线程的特点:
一般守护线程是一个死循环,所有的用户线程结束时,守护线程自动结束。
注意:主线程main方法是一个用户线程
//9、设置线程的优先级
t.setPriority(1); //设置t线程的优先级为1
注意:优先级范围【1,10】,默认为5。
优先级越高,获取CPU时间片的概率会高一些
//10、获取线程的优先级
t.getPriority();
4)定时器
4.1、定时器作用:间隔特定的时间,执行特定的程序
eg:每天进行数据的备份操作
4.2、实现方法:
第一种:使用sleep方法睡眠,设置睡眠时间,每到这个时间就执行任务。
这种方式是最原始的定时器。(不推荐)
第二种:Java类库中已经写好了一个定时器:java.util.Timer,直接调用就可以。(开发中也少用)
public static void main(String[] args) throws Exception {
//Timer t=new Timer(true); //守护线程
//创建定时器对象
Timer t = new Timer();
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date firstDate= sdf.parse("2021-03-30 18:20:00");
//void schedule(定时任务, 第一次执行时间, 每次多久执行一次)
//指定定时任务,每隔10秒执行一次
t.schedule(new MyTask(),firstDate,1000*10);
}
}
class MyTask extends TimerTask{
@Override
public void run() {
//定时执行的任务
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String str=sdf.format(new Date());
System.out.println(str+"完成一次备份");
}
}
第三种方式:使用spring框架中提供的SpringTask框架,来完成定时器
5)同步线程实现线程安全
5.1、什么时候数据在多线程并发的环境下会存在安全问题
满足以下三个条件:
1、多线程并发
2、有共享数据
3、共享数据有修改的行为
5.2、Java三大变量哪会存在安全问题
- 实例变量:在堆中
- 静态变量:在方法区
- 局部变量:在栈中
注意:
1、局部变量在栈中,栈中数据不共享,因此不存在安全问题
2、jvm只有一个堆内存和一个方法区。堆和方法区多是多线程共享的,所以可能会存在线程安全问题。
5.3、怎么解决线程安全问题
第一种方案:尽量使用局部变量代替实例变量和静态变量
第二种方案:如果一定要实例变量,可以考虑创建多个对象,那么实例变量的内存就不共享了。(一个线程对应一个对象,对象不共享,就没有数据安全问题)
第三种方案:当不能使用局部变量和创建多个对象时,只能使用synchronized了,线程同步机制
5.4、实现线程同步机制(synchronized)有三种写法
第一种:同步代码块
synchronized(线程共享对象){
同步代码块;
}
第二种:在实例方法上使用synchroized
缺点:整个方法体都需要同步,可能会扩大同步的范围,导致程序的执行效率降低
public synchronized void test(){
这种写法表示的共享对象一定是this(当前对象)
而且同步代码块是整个方法体
}
第三种:在静态方法上使用synchronized
public synchronized static void test(){
静态方法同一个类中的所有对象都共享
一个类只有一把类锁。
}
注意:在Java语言中,任何一个对象都有“一把锁”,锁其实是标记而已。(一个对象一把锁)
执行原理:
1、假设线程t1和t2并发,假设t1先遇到了synchronized关键字,这时自动找“后面共享对象”的对象锁,找到后,占有这把锁,然后执行同步代码块的程序,直到同步代码块执行完毕后,这把锁才会释放。
2、当t1占有这把锁后,t2也遇到了synchronized关键字,也会去占有“后面共享对象”的这把锁,但这把锁被t1占有,t2只能在同步代码块外面等待t1结束,当t1执行完同步代码块后,t1会归还这把锁,此时,t2终于等到这把锁,然后t2占有这把锁后,进入同步代码块执行程序。
6)死锁的实现
public static void main(String[] args) throws Exception {
Object o1=new Object();
Object o2=new Object();
MyThread8 t1=new MyThread8(o1,o2);
MyThread88 t2=new MyThread88(o1,o2);
t1.start();
t2.start();
}
}
class MyThread8 extends Thread{
Object o1;
Object o2;
public MyThread8(Object o1,Object o2){
this.o1=o1;
this.o2=o2;
}
@Override
public void run() {
synchronized(o1){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(o2){
}
}
}
}
class MyThread88 extends Thread{
Object o1;
Object o2;
public MyThread88(Object o1,Object o2){
this.o1=o1;
this.o2=o2;
}
@Override
public void run() {
synchronized(o2){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(o1){
}
}
}
}
实现原理:
t1先占有o1对象的锁,而t2先占有o2对象的锁,
然后t1、t2线程遇到第二个synchronized关键字时,
由于t1和t2各自占有了o1、o2的对象锁,
因此t1和t2线程只能等待着,因此被称为死锁。
7)关于Object类中的wait和notify方法(生产者和消费者模式)
7.1、什么是“生产者和消费者”模式
生产线程负责生产,消费线程负责消费
生产线程和消费线程要达到均衡
实现这种模式需要使用wait和notify方法
7.2、wait和notify方法不是线程对象的方法,是普通Java对象都有的方法
7.3、wait和notify方法建立在线程同步的基础上。因为多线程要同时操作一个仓库,才能实现生产者和消费者模式
7.4、wait方法的作用:o.wait()让正在o对象上活动的线程t进入等待状态,并且释放t线程之前占有o对象的锁
7.5、notify方法作用:o.notify()让正在o对象上等待的线程唤醒,只是通知,不会释放o对象上之前占有的锁。
7.6、notifyAll方法作用:唤醒o对象上处于等待的所有线程