文章目录
多线程的概念
1. 什么是进程?什么是线程?
进程是一个应用程序(1个进程是一个软件)。
线程是一个进程中的执行场景/执行单元。
一个进程可以启动多个线程。
2. 什么是进程和线程的例子
对于java程序来说,当在DOS命令窗口中输入:
java HelloWorld 回车之后。
会先启动JVM,而JVM就是一个进程。
JVM再启动一个主线程调用main方法。
同时再启动一个垃圾回收线程负责看护,回收垃圾。
最起码,现在的java程序中至少有两个线程并发,
一个是垃圾回收线程,一个是执行main方法的主线程。
3. 对于单核的CPU来说,真的可以做到真正的多线程并发吗?
单核的CPU表示只有一个大脑:
不能够做到真正的多线程并发,但是可以做到给人一种“多线程并发”的感觉。
对于单核的CPU来说,在某一个时间点上实际上只能处理一件事情,但是由于
CPU的处理速度极快,多个线程之间频繁切换执行,跟人来的感觉是:多个事情
同时在做!!!!!
线程A:播放音乐
线程B:运行魔兽游戏
线程A和线程B频繁切换执行,人类会感觉音乐一直在播放,游戏一直在运行,
给我们的感觉是同时并发的。
4. 真正的多线程是指有多个cpu,即多核
对于多核的CPU电脑来说,真正的多线程并发是没问题的。
4核CPU表示同一个时间点上,可以真正的有4个进程并发执行。
创建线程的几种方式
第一种方式:编写一个类,直接继承java.lang.Thread,重写run方法。
package test;
public class TheadTest {
public static void main(String[] args) {
StartThread startThread = new StartThread();
startThread.start();
for (int i = 0; i < 20; i++) {
System.out.println("我正在摸鱼!");
}
}
}
package test;
public class StartThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("我正在听课!");
}
}
}
常用的几种方法说明
start方法的作用:启动一个分支线程,在jvm中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。
这段代码的任务是为了开启一个新的栈空间.只要新的栈空间开出来,start()。方法就结束。线程就启动成功了。
启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)。
run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main是平级的
注意:
- 方法体是自上而下运行的
- 直接调用run()是没有开启多线程还是main主线程、要开多线程必须调用start()方法,start()方法当开辟栈空间后就结束,线程会自动调用run方法
直接调用run和start方法的区别
start方法内存使用情况
直接调用run方法内存使用情况
第二种方式:编写一个类,实现java.lang.Runnable接口,实现run方法。
一般使用匿名内部类写线程
public class RunnableTest {
public static void main(String[] args) {
/*MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
for (int i = 0; i < 1000; i++) {
System.out.println("主线程:我在摸鱼!");
}*/
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("主线程:我在摸鱼!");
}
}
});
thread.start();
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("分支线程:我正在听课!");
}
}
}
线程的生命周期
线程的常用方法
静态方法:
获取当前线程
public static Thread currentThread()
作用:返回对当前正在执行的线程对象的引用。
使用:将他放在某个方法就返回对当前正在执行的线程对象的引用,所以一般放在run方法内。- 当前对象线程对象,当前对象是谁?
- 当t1线程对象,调用了run方法,则当前线程是t1的
- 当t2线程对象,调用了run方法,则当前线程是t2的
让当前线程睡眠(停止x秒)
static void sleep(long millis, int nanos)
static void sleep(long millis)
- 方法参数
- millis毫秒,睡眠几毫秒
作用
在指定的毫秒数内让当前正在执行的线程休眠(暂停执行、进入阻塞状态),此操作受到系统计时器和调度程序精度和准确性的影响。
问题:以下代码在ThreadTest类中使用StartThread类型的对象t,调用sleep方法,是让t进入睡眠嘛?
使用场景 sleep可以模拟网络延时,倒计时等。
- millis毫秒,睡眠几毫秒
package test;
public class ThreadTest {
public static void main(String[] args) {
StartThread t = new StartThread();
t.setName("t111");
t.start();
try {
t.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("摸鱼!");
}
}
package test;
public class StartThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":正在摸鱼!");
}
}
结果:进入睡眠的是main,不是t的线程,因为t.sleep(1000*5)
是静态方法,与对象无关,t.sleep(1000*5)
等价于Thread.sleep(1000*5)
与线程调度有关系的方法:
让位方法
static void yield()
作用:让当前线程让位,让给其它线程使用。
注意:
- yield()方法不是阻塞方法。
- yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。
void setPriority(int newPriority)
设置线程的优先级int getPriority()
获取线程优先级
最低优先级1
默认优先级是5
最高优先级10(优先级比较高的获取CPU时间片可能会多一些。(但也不完全是,大概率是多的。))
实例方法:
等待当前线程终止
void join()
**作用:**如果一个main线程执行了 threadA.join() 语句,其含义是:当前main线程等待 threadA 线程终止之后才从 threadA .join() 返回继续往下执行自己的代码。
关于多线程并发环境下,数据的安全问题。(重点 )
-
为什么这个是重点?
以后在开发中,我们的项目都是运行在服务器当中,
而服务器已经将线程的定义,线程对象的创建,线程
的启动等,都已经实现完了。这些代码我们都不需要
编写。(其实也会自己用到线程池,这段话可以听个大概)
最重要的是:你编写的程序需要放到一个多线程的环境下运行,你更需要关注的是这些数据在多线程并发的环境下是否是安全的。 -
什么时候数据在多线程并发的环境下会存在安全问题呢?
三个条件:
条件1:多线程并发。
条件2:有共享数据。
条件3:共享数据有修改的行为。
满足以上3个条件之后,就会存在线程安全问题。 -
怎么解决线程安全问题呢?
线程同步机制:线程排队执行。(不能并发),用排队执行解决线程安全问题。 -
线程同步机制的语法
synchronized(共享对象){
//线程同步代码块
}
- 共享对象是什么意思
共享对象是线程共同操作(作用)的对象,叫共享对象,这里设置的共享对象就像锁,t1,t2,t3共同操作作用同一一个account对象,使用synchronized(this)
设置的共享对象就是只有线程t1、t2要去拿对象锁,t1如果这个对象锁没被使用,直接就能进入同步代码块,当执行完毕后释放对象锁,t2拿到对象锁才能执行同步代码块,以此类推,同不同步看共享对象。
以下代码的执行原理?
- 假如t1、t2线程并发,开始执行以下代码的时候,肯定有一个先一个后。
- 假设t1先执行了,遇到了synchronized ,这个时候自动找“后面共享对象”的对象锁,找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是占有这把锁的。直到同步代码块代码结束,这把领才会释放。
- 假设t1已经占有这把锁,此时t2也遇到synchronised关键字,也会去占有后面共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束,
直到tT把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁.然后t2占有这把领之后,进入同步代码块执行程序,这样就达到了线程排队执行。
这里需要注意的是:这个共享对象一定要选好了。这个共享对象一定是你需要排队,执行的这些线程对象所共享的。对象锁都在锁池
使用synchronized几种方式
-
实例方法上可以使用synchronized
synchronized出现在实例方法上,一定锁的是this。没得挑。只能是this。不能是其他的对象了。所以这种方式不灵活。- 缺点
表示整个方法体都需要同步,可能会无故扩大同步的范圉,导致程序的执行效率降低。所以这种方式不常用 - 优点
使用简洁
- 缺点
-
在静态方法上使用synchronized
表示找类锁.
类锁永远只有1把.
就算创建了 100个对象,那类锁也只有一把 -
类锁与对象锁的区别
对象锁:1个对象1把锁,100个对象100把锁。
类锁:100个对象,也可能只是1把类锁。
对象锁(synchronized method{})和类锁(static sychronized method{})的区别。摘自:Kafka不卡
对象锁也叫实例锁,对应synchronized关键字,当多个线程访问多个实例时,它们互不干扰,每个对象都拥有自己的锁,如果是单例模式下,那么就是变成和类锁一样的功能。
对象锁防止在同一个时刻多个线程访问同一个对象的synchronized块。如果不是同一个对象就没有这样子的限制。
类锁对应的关键字是static sychronized,是一个全局锁,无论多少个对象是否共享同一个锁(也可以锁定在该类的class上或者是classloader对象上),同样是保障同一个时刻多个线程同时访问同一个synchronized块,当一个线程在访问时,其他的线程等待。
什么是守护线程?
java语言中线程分为两大类
一类是:用户线程
一类是:守护线程(后台线程)
其中具有代表性的就是:垃圾回收线程(守护线程)。
守护线程的特点
- 一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。
守护线程的作用
每天00:00的时候系统数据自动备份。这个需要使用到定时器,并且我们可以将定时器设置为守护线程。一直在那里看着,没到00:00的时候就备份一次。所有的用户线程如果结束了,守护线程自动退出,没有必要进行数据备份了。
注意:主线程main方法是一个用户线程。
如何使用守护线程呢
- 在使用
start()
方法之前,使用线程对象.setDaemon(true)
,设置为守护线程,不管守护线程的run()
的方法体是死循环都能停下来的。
定时器(重要)
定时器的作用
- 间隔特定的时间,执行特定的程序。
- 例子:每周要进行银行账户的总账操作。
每天要进行数据的备份操作。
实际的开发中,每隔多久执行一段特定的程序有多种方式实现(知道有哪些方法就行了,了解即可)
- 可以使用sleep方法,睡眠,设置睡眠时间,没到这个时间点醒来,执行
任务。这种方式是最原始的定时器。 - 在java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用。
不过,这种方式在目前的开发中也很少用,因为现在有很多高级框架都是支持定时任务的。 - 在实际的开发中,目前使用较多的是Spring框架中提供的SpringTask框架,这个框架只要进行简单的配置,就可以完成定时器的任务。
定时器的使用
实现线程的第三种方式::实现Callable接口。(JDK8新特性。)
为什么这么多实现线程的方法还在jdk8更新以下实现线程的方式呢?
- 因为这种方式实现的线程可以获取线程的返回值,之前那两种方式是无法获取线程返回值的,因为run方法返回void。此实现方式是为了根据需求需要线程返回某返回值。
Callable接口实现线程的使用
注意:在使用FutureTask对象名.get()
获取返回值对象时,会堵塞当前线程(main线程),等待返回对象才结束堵塞。
关于Object类中的wait和notify方法。(生产者和消费者模式!)
注意::wait和notify方法不是线程对象的方法,是java中任何一个java对象都有的方法,因为这两个方式是Object类中自带的。wait方法和notify方法不是通过线程对象调用,不是这样的:t.wait(),也不是这样的:t.notify()…不对。