多线程
进程与线程的关系
- 进程是一个应用程序,线程是一个进程中的执行场景/执行单元。
- 进程与进程之间是独立不共享的。
- 一个进程可以启动多个线程。
对于java程序来说,在dos命令窗口中输入:java HelloWorld 回车之后,会先启动JVM,而JVM就是一个进程。JVM启动一个主线程调用main方法,同时再启动一个垃圾回收线程负责看护,回收垃圾。由此可知,java程序中至少有两个线程并发。
Java中的进程与线程
- 在java语言中,线程A和线程B,堆内存和方法区内存共享,但是栈内存独立,一个线程一个栈,可以多个线程并发执行(提高程序运行效率)。
- 使用了多线程机制后,main方法结束程序也不一定结束,main方法结束说明主线程序结束,主栈空了,其它的栈(线程)可能还在压栈/弹栈。
- 单核的CPU不能够做到真正的多线程并发,但是可以做到给人一种“多线程并发”的感觉,对于单程的CPU来说,在某一个时间点上实际上只能处理一件事,但是由于CPU的处理速度极快,多个线程之间频繁切换执行,给别人的感觉就是:多个事情同时在做;而多核的CPU就可以多个进程同时进行。
实现多线程的三种方式
- 第一种方式:编写一个类,直接继承java.lang.Thread ,重写run方法。
具体过程:定义线程类(extends Thread) —> 创建线程对象(new) —> 启动线程(start)
public class ThreadTest01 {
public static void main(String[] args) {
// 这里是main方法,这里的代码属于主线程,在主线中运行
// 新建一个分支线程
MyThread myThread = new MyThread();
// 启动线程
/*
start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,
这段代码任务完成之后,瞬间就结束了。这行代码的任务只是为了开启一个新的
栈空间,只要新的栈空间开出来了,start方法就结束了,线程启动成功。启动
成功的线程会自动调用run方法,并且run方法在分支线的栈底部(压栈)。
run方法在分支栈的栈底部,main方法在主栈的栈底部,run和main是平级的。
*/
myThread.start();
// myThread.run(); 直接调用run方法不会启动线程,不会分配新的分支栈(这种方式是单线程)
// 这里的代码还是运行在主线程中
for (int i = 0;i < 1000;i++){
System.out.println("主线程--->" + i);
}
}
}
class MyThread extends Thread{
@Override
public void run() {
// 编写程序,这段程序运行在分支线程中(分支线)
for (int i = 0;i < 1000;i++){
System.out.println("分支线程--->" + i);
}
}
}
- 第二种方式:编写一个类,实现java.lang.Runnable —> 创建线程对象(new) —> 启动线程(start)
【注意】第二种方式比较实用,因为一个类实现了接口,还能继承其他类,更灵活。
public class ThreadTest02 {
public static void main(String[] args) {
// 创建一个可运行的对象
MyRunnable r = new MyRunnable();
// 将可运行的对象封装成一个线程对象
Thread t = new Thread(r); // 构造方法:Thread(Runnable target)
// Thread t = new Thread(new MyRunnable()); // 合并代码
// 启动线程
t.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 ThreadTest03 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
}
});
}
}
- 第三种方式:FutureTask方式,实现Callable接口。(JDK8新特性)
这种方式实现的线程可以获取线程的返回值,前两种方式是无法获取线程的返回值的。
优点:可以获取线程的执行结果
缺点:效率较低,当获取另一个线程的执行结果时,可能会导致当前线程受阻塞
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask; // JUC包下的,属于java的并发包,老JDK中没有这个包,新特性
/*
实现线程的第三种方式:实现Callable接口。
*/
public class ThreadTest13 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 第一步:创建一个未来任务类
FutureTask task = new FutureTask(new Callable() {
@Override
public Object call() throws Exception {
System.out.println("call method begin !");
Thread.sleep(1000 * 3);
System.out.println("call method end !");
int a = 100,b = 200;
return a + b ; // 这里存在自动装箱,返回类型原来为Object
}
});
Thread t = new Thread(task);
t.start();
// 由于当前是在主线程中,在主线程中获取t线程的返回执行结果,可能会导致“当前”线程阻塞
Object obj = task.get();
System.out.println("线程执行结果:" + obj);
// main方法执行到这里,必须等待get方法结束,因为get方法是为了取一个线程的执行结果
// 只要线程结果不出,main方法等待的时间就会很长
System.out.println("Hello World!");
线程的生命周期
如图所示
在线程的生命周期中,需要掌握线程五个状态:新建状态、就绪状态、运行状态、阻塞状态、死亡状态
如何让线程进入阻塞状态(sleep)
- 静态方法:Thread.sleep(100)
- 参数是毫秒
- 作用:让当前线程进入休眠,进入“阻塞”状态,放弃占有CPU时间片,让给其他进程使用。
- Thread.sleep方法可以实现:间隔特定的时间,执行一段特定的代码,每隔多久执行一次。
/*
线程的阻塞状态,关于线程的sleep方法:static void sleep(long millis)测试
*/
public class ThreadTest05 {
public static void main(String[] args) {
/*
3s后输出 Hello World !
try {
Thread.sleep(1000 * 3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Hello World !");
*/
// 以下程序每隔1s输出一次
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/*sleep方法是让当前线程进入休眠,测试如下:*/
public class ThreadTest06 {
public static void main(String[] args) {
Thread t = new MyThread3();
t.setName("t1");
t.start();
try {
/* 由于sleep方法是让当前线程进入休眠,也就是说main线程进入休眠。(因为sleep是静态方法,调用静态方法与对象无关)*/
t.sleep(1000 * 3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class MyThread3 extends Thread{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println(i);
}
}
}
中断进入睡眠状态的线程(Interrupt)
中断线程睡眠状态:调用Interrupt方法(这种中断睡眠的方式依靠的是java的异常处理机制)
/*
如何中断进入睡眠状态的线程
*/
public class ThreadTest07 {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable2());
thread.setName("t");
thread.start();
try {
Thread.sleep(1000*3);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 中断thread线程的睡眠(这种中断睡眠的方式依靠的是java的异常处理机制)
thread.interrupt();
}
}
class MyRunnable2 implements Runnable{
// 这里的sleep方法不能上抛异常的原因:
// run方法在父类中没有做出任何异常,子类不能比父类抛出更多的异常
// (不是从父类中继承的方法可以上抛)
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " --> begin");
try {
Thread.sleep(1000*60*60*24*365);
} catch (InterruptedException e) {
// 这里会在中断后打印中断异常信息
// java.lang.InterruptedException: sleep interrupted
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " --> end");
}
}
强行终止线程执行(stop)
/*
在java中强行终止一个线程的执行:stop()
这种方式存在很大的缺点:容易丢失数据,因为这种方式是直接将线程杀死了,
线程没有保存的数据将会丢失,不建议使用。
*/
public class ThreadTest08 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable3());
t.start();
// 模拟5s
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 5s之后强行终止
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 ThreadTest09 {
public static void main(String[] args) {
MyRunnable4 r = new MyRunnable4();
Thread t = new Thread(r);
t.start();
try {
Thread.sleep(1000 * 3);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 终止线程
r.run = false;
}
}
class MyRunnable4 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 {
// 终止当前线程
// 可以在return前进行保存操作
return;
}
}
}
}
线程让位(yield)
虽然线程让位可以让当前线程暂停,回到就绪状态,让给其他线程,但是有可能出现当前线程回到就绪状态后立马又在就绪状态时抢到了时间片,继续回到运行状态。
/*
让位,当前线程暂停,回到就绪状态,让给其他线程
*/
public class ThreadTest10 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable6());
t.start();
t.setName("t");
for (int i = 0; i < 1000; i++) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}
class MyRunnable6 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
// 每100个让位一次
if (i % 100 ==0){
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}
线程合并(join)
public class ThreadTest11 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable7());
t.setName("t");
System.out.println(Thread.currentThread().getName() + " begin!");
t.start();
// 合并线程
try {
t.join();// t合并到当前线程中,当前线程受到阻塞,直到t线程执行结束
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " end!");
}
}
class MyRunnable7 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
}
线程的调度
- 常见的线程调度模型有哪些?
- 抢占式调度模型:哪个线程的优先级比较高,抢到时间片的概率就高一些/多一些。【java就是采用这种模型】
- 均分式调度模型:平均分配CPU时间片,每个线程占有的CPU时间片时间长度一样,平均分配,一切平等。
- java中提供的与线程调度有关系的方法:
最低优先级1;默认优先级是5;最高优先级10(优先级比较高的大概率能获取更多的时间片)
-
实例方法:
- void setPriority(int newPriority) 设置线程的优先级
- int getPriority() 获取线程优先级
- void join() 合并线程 当前线程进入阻塞,引入新线程执行完毕后再运行。(本质是栈和栈之间发生了等待让位,栈内存没有合并)
-
静态方法:
- static void yield() 让位方法:暂停当前正在执行的线程对象,并执行其他线程。注意:yield方法不是阻塞方法,让当前线程让位,让给其他线程使用,yield方法的执行会让当前线程从运行状态回到就绪状态。
多线程并发环境下数据的安全问题
以后在开发中,我们的项目都是运行在服务器中,而服务器已经将线程的定义,线程对象的创建,线程的启动等,都已经实现了,这些代码不需要我们编写。所以,最重要的是:要了解你编写的程序需要放到一个多线程的环境下运行,你更需要关注的问题是这些数据在多线程并发的 环境下是否是安全的。
-
数据在多线程并发的情况下会出现安全问题的三个条件:
- ①多线程并发
- ②有共享数据
- ③共享数据有修改的行为
-
如何解决线程安全问题?
当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就存在线程安全问题,怎么解决这个问题?线程排队执行(不能并发),用排队执行解决线程安全问题,这种机制被称为:线程同步机制。由于线程排队执行,线程会牺牲部分效率,不过数据安全第一位,没有数据安全,谈何效率。
-
掌握两个专业术语:
异步就是并发,同步就是排队
- 异步编程模型:线程t1和线程t2,各自执行各自的,谁也不需要等谁。其实就是:多线程并发。(效率较高)
- 同步编程模型:线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行结束,两个线程之间发生了等待关系,就是同步编程模型。(效率较低)
变量与安全问题
Java中存在三大变量:实例变量(在堆中)、静态变量(在方法区)、局部变量(在栈中)。
如果使用局部变量的话:建议使用StringBuilder,因为局部变量不存在线程安全,选用StringBuffer效率较低。
-
局部变量永远都不会存在线程安全问题,因为局部变量不共享(一个线程一个栈)。而堆和方法区是多线程共享的,所以可能存在线程安全的问题。
-
局部变量 + 常量:不会有线程安全问题
-
成员变量:可能会有线程安全问题。
【注】ArrayList是非线程安全的;Vector是线程安全的;HashMap、HashSet是非线程安全的,HashTable是线程安全的。
synchronized的三种实现方式
- 第一种:同步代码块(灵活)
synchronized(共享对象){
同步代码块;
}
- 第二种:在静态方法上使用synchronized,表示共享对象一定是this,并且同步代码块是整个方法体。
public synchronized 返回值类型 方法名(参数列表){
}
- 第三种:在静态方法上使用synchronized,表示找类锁,一个类有且只有一把锁。
死锁
synchronized在开发中最好不要嵌套使用,容易出现死锁。
/*
Deadlock要会写,一般面试官会要求,因为死锁很难调试.
总结:synchronized在开发中最好不要循环使用,容易出现死锁。
*/
public class DeadLock {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
// 两个线程共享o1,o2
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){
}
}
}
}
在开发中如何解决线程安全问题?
在开发中遇到线程安全问题时,不能直接使用synchronized关键字,因为synchronized会让程序的执行效率较低,用户体验不好,系统的用户吞吐量(并发量)降低,用户体验差,只有在不得已的情况下再选择线程同步机制。
那么其它情况下要如何解决呢?
-
第一种方案:尽量使用局部变量代替实例变量和静态变量。
-
第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(一个线程对一个对象)
-
第三种方案:如果不能够使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized了,线程同步机制。
守护(后台)线程
java语言中线程分为两大类:用户线程和守护线程。
守护线程的代表:垃圾回收线程;(main方法是一个用户线程)
设置守护线程要在线程启动之前设置,只要用户线程结束,守护线程就一定会结束。
- 守护线程的特点:一般守护线程是一个死循环,所有的用户线程都结束了,守护线程自动结束。
- 守护线程的用途:比如——每天00:00的时候数据自动备份。这个需要使用定时器,并且可以将定时器设置为守护线程。
/*
设置守护线程:setDaemon()
*/
public class ThreadTest12 {
public static void main(String[] args) {
Thread t1 = new BakDataThread();
t1.setName("t1");
// 启动线程之前,将线程设置为守护线程
// 守护线程,不管BakDataThread里的run方法是不是死循环,只要用户线程结束,守护线程就会结束
t1.setDaemon(true);
t1.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();
}
}
}
}
定时器
-
作用:间隔特定的时间,执行特定的程序。
-
实现方式:
- ①可以使用sleep方法,设置睡眠时间,这是最原始的定时器(比较low)。
-
②使用java.util.Timer。不过这种方式使用也比较少,因为现在有很多高级框架都是支持定时任务的。在实际的开发中,目前使用的最多的是Spring框架中提供的SpringTask框架,只需要简单的配置,就可以完成定时器的任务。
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
/*
使用定时器指定定时任务
*/
public class TimerTest {
public static void main(String[] args) throws ParseException {
Timer timer = new Timer();
// Timer timer1 = new Timer(true); // 表示以守护线程的形式存在
// 指定定时任务
// timer.schedule(定时任务,第一次执行时间,间隔多久执行一次);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
Date firstTime = sdf.parse("2020年07月26日 22:49:30");
timer.schedule(new MyTimerTask(),firstTime,1000*5);
// 也可以采用匿名内部类的方式编写定时器任务
}
}
// TimerTask是一个抽象类,必须要实现抽象类中的方法
// 假设这是一个记录日志的定时任务
class MyTimerTask extends TimerTask{
@Override
public void run() {
// run方法中编写要执行的任务
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
String strTime = sdf.format(new Date());
System.out.println(strTime + ",成功完成了一次数据备份!");
}
}
生产者和消费者模式
Java中的生产者和消费者模式主要是指Object类中的wait方法和notify方法。
生产者和消费者模式是为了专门解决某个特定的需求,最终要达到生产和消费必须均衡。
-
第一:wait和notify方法不是线程对象的方法,是java中任何一个java对象都有的方法,因为是Object类自带的。(wait 和 notify方法不是通过线程对象调用的)
-
第二:wait() 方法有什么作用?
// 以下代码表示:让正在o对象上活动的线程进入等待状态,无期限等待,直到被唤醒为止。
// o.wait方法的调用会让“当前线程”进入等待状态,并且释放之前占有的o对象的锁。
Object obj = new Object();
o.wait();
- 第三:notify() 方法有什么作用?
唤醒正在o 对象上等待的线程,notify只会通知,Builder释放之前占有的o对象的锁。还有一个notifyAll方法:这个方法是唤醒o对象上处于等待的所有线程。
wait方法和notify方法建立在synchronized线程同步的基础之上。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W0CiDMCo-1596075558276)(http://m.qpic.cn/psc?/V50XxdkH1tPwSK3Uuk5h1NQBET0EINP0/ruAMsa53pVQWN7FLK88i5usW1iPLEAN0ncRVSlm1vtiOYznjqvqtpqP6h7MYZmqYUP5HVIcIdGvjjnyH3M6*jlhcQ4sq3eVd2xw8Auj0I9A!/b&bo=6AJvAegCbwEDCSw!&rf=viewer_4 “生产者和消费者模型” )]
import java.util.ArrayList;
import java.util.List;
/*
模拟生产者和消费者的需求:
多线程同时使用一个仓库,仓库采用List集合
假设List集合只能存储1个元素,当存储1个元素是仓库为满,
当存储0个仓库为空。
即:必须做到生产1个消费1个
*/
public class ThreadTest14 {
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() {
// 一直生产,用死循环来模拟
while (true) {
// 给仓库对象list加锁
synchronized (list){
if (list.size() > 0){ // 大于0,说明仓库中已经有1个元素了
try {
// 仓库已满,当前进程进入等待状态,并且释放Producer之前占有的锁
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序执行到这里,说明仓库是空的,可以生产
Object obj = new Object();
list.add(obj);
System.out.println(Thread.currentThread().getName() + "--->" + obj);
// 唤醒消费者进行消费
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) { // 等于0,说明仓库已清空
try {
// 仓库已清空,当前进程进入等待状态,并且释放Consumer占有的锁
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序执行到这里,说明仓库不是空的,可以消费
Object obj = list.remove(0); // 移除下标为0的list集合元素
System.out.println(Thread.currentThread().getName() + "--->" + obj);
// 唤醒生产者进行生产
list.notify();
}
}
}
}