多线程
多线程概述
进程与线程
- 进程
是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间
- 线程
是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行. 一个进程最少
有一个线程
线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分
成若干个线程
线程调度
- 分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间
- 抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核新而言,某个时刻,
只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是 在同一时
刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的 使
用率更高。
同步与异步
- 同步
排队执行 , 效率低但是安全.
- 异步
同时执行 , 效率高但是数据不安全
多线程内存图
- 一个线程一个栈空间,共享堆内存和方法区
实现线程的方式
第一种方式:继承Thread类,重写run方法
public class MyThread {
public static void main(String[] args) {
ThreadTest01 test01 = new ThreadTest01();
test01.start();//start()方法的作用:启动一个分支线程,在JVM中开辟新的栈空间
//这里的代码运行在主线程main方法中
for (int i=0;i<100;i++){
System.out.println("主线程->"+i);
}
}
}
class ThreadTest01 extends Thread{
@Override
public void run() {//run()方法和main方法平级,都在栈的底部.
for (int i=0;i<100;i++){
System.out.println("分支线程->"+i);
}
}
}
- 注意:start()方法的作用:启动一个分支线程,在JVM中开辟新的栈空间,任务完成后,这个方法瞬间就结束了,启动线程成功后会自动调用run()方法,并且run()方法在分支栈的底部(压栈)。
第二种方式:实现Runnable接口,实现run方法
public class ThreadTest02 {
public static void main(String[] args) {
/* MyRunnable mr = new MyRunnable();
Thread tr = new Thread();*/
//上面两行代码合并如下
Thread tr = new Thread(new MyRunnable());
tr.start();
for (int i=0;i<100;i++){
System.out.println("主线程->"+i);
}
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i=0;i<100;i++){
System.out.println("分支线程->"+i);
}
}
}
- 匿名内部类创建线程:
public class ThreadTest02 {
public static void main(String[] args) {
//匿名内部类的方式
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i=0;i<100;i++){
System.out.println("分支线程->"+i);
}
}
});
t.start();
for (int i=0;i<100;i++){
System.out.println("主线程->"+i);
}
}
}
线程的常用方法
public class ThreadTest03 implements Runnable {
public static void main(String[] args) {
Thread r = new Thread(new ThreadTest03());
r.start();//创建线程
r.setName("分支线程一:");//修改线程名字
String name = r.getName();//获取线程名字
//获取当前线程,在mian方法中,当前线程就是main
Thread currentThread = Thread.currentThread();
System.out.println(currentThread.getName());//获取当前线程的名字。
try {
/*静态方法,单位是毫秒。
让当前(main)线程进去休眠,进去“阻塞状态”,
放弃占用CPU时间片,让给其他线程使用*/
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//中断线程睡眠
r.interrupt();
}
@Override
public void run() {
}
}
线程的生命周期
线程同步机制
synchronized语法
- synchronized的()写什么?
假设 t1, t2, t3, t4,t5,有5个线程。你只希望t1,t2,t3排队,t4,t5不排队。
()里面一定要写t1,t2,t3的共享对象。而这个共享对象对t4,t5不共享。
//第一种:同步代码块越小效率越高
synchronized (线程共享对象){
}
}
//第二种:实体在实体方法上使用synchronized,
表示共享对象一定是This,并且同步代码块是整个方法体
第三种:在静态方法上使用synchronized,表示类锁,
就算创建100个对象,类也只有一把锁。
对象锁:1个对象一把锁,100个对象100把锁。
类锁:100个对象也可能只是一把类锁
那些变量存在线程安全问题:
- JAVA三大变量:
1:实例变量:在堆区
2:静态变量:在方法区
3:局部变量:在栈中
以上三大变量中:
局部变量永远都不会存在线程安全问题。因为局部变量不共享。
一个线程一个栈,局部变量在栈中,不共享。
实例变量在堆区,堆区只有一个。
静态变量在方法区,方法区只有一个。
堆区和方法区都是多线程共享的,所以存在线程安全问题。
死锁
- 死锁指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
- 生活中的一个实例,两个人面对面过独木桥,甲和乙都已经在桥上走了一段距离,即占用了桥的资源,甲如果想通过独木桥的话,乙必须退出桥面让出桥的资源,让甲通过,但是乙不服,为什么让我先退出去,我还想先过去呢,于是就僵持不下,导致谁也过不了桥,这就是死锁。
开发中应该如何解决线程安全问题?
- synchronized会让程序执行效率降低,用户体验不好。系统用户的吞吐量降低,用户体验差。在不得已的情况下选择synchronized。
- 尽量使用局部变量代替“实例变量”和“静态变量”。
- 如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(一个线程对象一个对象,100个线程对象100个对象,对象不共享就没有数据安全问题)
- 如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized了。线程同步机制。
守护线程
-
JAVA线程分为两大类:
1:用户线程
2:守护线程(后台线程)
其中具有代表性的就是:垃圾回收线程(守护线程)。 -
收获线程的特点:
一般守护线程是一个死循环,所以的用户线程只要结束,守护线程自动结束。
注意:主线程main方法是一个用户的线程。 -
守护线程可以用到什么地方呢?
比如每天00:00的时候系统数据自动备份。
这个需要使用到定时器,并且我们可以将定时器设置为守护线程。所以用户线程结束,守护线程自动退出。 -
定时器
间隔特定的时间,执行特定的程序。比如:每周要进行银行账户总账操作。每天要进行数据的备份。
在实际开发中,每隔多久执行一段特定的程序,这种需求很常见,那么在JAVA中其实可以采用多种方式实现:
可以使用sleep()方法,设置睡眠时间,这种方式是最原始的定时器(比较LOW)
在JAVA类库中已经写好了一个定时器:util .Timer,可直接拿来使用。不过这种方式目前开发也很少用。因为有许多框架支持定时任务。
在实际开发中,目前使用较多的是Spring框架中提供的SpringTask框架,这个框架只需要进行简单的配置,就可以完成定时任务。
关于Object类中的wait和notify方法(生产者与消费者模式)
- wait和notify方法不是线程对象的方法,是JAVA中任何一个对象都用的方法,因为这两个方式是Object类中自带的。
- wait和notify方法不是通过线程对象调用的。
wait()方法的作用:
Object 0 = new Object
0.wait():
表示:让正在O对象上活动的线程进入等待状态,无限期等待,直到被唤醒为止。
notity()方法的作用
Object 0 = new Object
0.notity():
表示:唤醒正在O对象上等待的线程.
0.notityAll():唤醒正在O对象上等待的所以线程.