[8w字] | Java多线程全套功法

Posted 结构化思维wz

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[8w字] | Java多线程全套功法相关的知识,希望对你有一定的参考价值。

介绍:CSDN统计字数:77153字,Java多线程从入门到精通,由浅入深。[建议收藏!]
❤️❤️❤️❤️❤️❤️ 点击主页发现更多好文❤️❤️❤️❤️❤️❤️

文章目录

引言

什么是进程,线程?

进程资源分配的最小单位,cpu从磁盘中读取一段程序到内存中,该执行程序的实例就叫做进程。一个程序如果被cpu多次读取到内存中,则变成多个独立的进程。

线程 : 线程是程序执行的最小单位,在一个进程中可以有多个不同的线程。

线程的应用实例:

同一个应用程序中(进程),更好的并行处理。

例子:手写一个文本编辑器需要多少个线程?

为什么需要使用多线程?

采用多线程的形式执行代码,目的是为了提高程序开发的效率。

串行、并行的区别

CPU分时间片交替执行,宏观并行,微观串行,由OS负责调度。如今的CPU已经发展到了多核CPU,真正存在并行。

CPU调度算法

多线程是不是一定提高效率? 不一定,需要了解cpu调度的算法。

CPU调度算法:

如果在生产环境中,开启很多线程,但是我们的服务器核数很低,我们这么多线程会在cpu上做上下文切换,反而会降低效率。

使用线程池来限制线程数和cpu数相同会比较好。

一、Java多线程基础

创建线程的方式

  1. 继承Thread类创建线程
  2. 实现Runnable接口创建线程
  3. 使用匿名内部类形式创建线程
  4. 使用Lambda表达式创建
  5. 使用CallableFuture创建线程
  6. 使用线程池创建
  7. Spring中的@Async创建

1.继承Thread类创建线程


public class ThreadTest extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("子线程,线程一");
        }

    }
    
/* 创建对象进入初始状态,调用start()进入就绪状态。直接调用run()方法,相当于在main中执行run。并不是新线程*/
    public static void main(String[] args) {
        new ThreadTest().start();

    }
}

2.实现Runnable接口创建线程

public class Thread02 implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"我是子线程");
    }
    public static void main(String[] args) {
        new Thread(new Thread02()).start();
    }
}

3.使用匿名内部类形式创建线程

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"我是子线程");
            }
        }).start();
    }

4.使用Lambda表达式创建

public class Thread02  {
    public static void main(String[] args) {
        new Thread(() -> System.out.println(Thread.currentThread().getName()+"我是子线程")).start();
    }
}

5.使用CallableFuture创建线程

Callable和Future线程可以获取到返回结果,底层基于LockSupport。 (这里只是略写,后面有详细介绍)

Runnable的缺点:
1.  run没有返回值
2.  不能抛异常
    
Callable接口允许线程有返回值,也允许线程抛出异常
Future接口用来接受返回值
public class Thread03 implements Callable<Integer> {
    /**
     * 当前线程需要执行的代码,返回结果
     * @return 1
     * @throws Exception
     */
    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName()+"返回1");
        return 1;
    }
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
    Thread03 callable = new Thread03();
    FutureTask<Integer> integerFutureTask = new FutureTask<Integer>(callable);
    new Thread(integerFutureTask).start();
    //通过api获取返回结果,主线程需要等待子线程返回结果
    Integer result = integerFutureTask.get();
    System.out.println(Thread.currentThread().getName()+","+result); // main,1
}

6.使用线程池创建

public class ThreadExecutor {
    public static void main(String[] args) {
     ExecutorService executorService = Executors.newCachedThreadPool();
     executorService.execute(new Runnable() {
         @Override
         public void run() {
             System.out.println(Thread.currentThread().getName()+"我是子线程1");
         }
     });
     executorService.submit(new Thread03()); //submit一个线程到线程池
    }
}

7.Spring中的@Async创建

第一步:在入口类中开启异步注解

@SpringBootApplication
@EnableAsync

第二步:在当前方法上加上@Async

@Component
@Slf4j
public class Thread01 {
    @Async
    public void asyncLog(){
        try {
            Thread.sleep(3000);
            log.info("<2>");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

第三步:验证测试


@RestController
@Slf4j
public class Service {
    @Autowired
    private Thread01 thread01;
    @RequestMapping("test")
    public String Test(){
        log.info("<1>");
        thread01.asyncLog();
        log.info("<3>");
        return "test";
    }
}

访问localhost:8080/test查看日志为:

Thread中的常用的方法

1.Thread.currentThread() 方法可以获得当前线程

java中的任何一段代码都是执行在某个线程当中的,执行当前代码的线程就是当前线程。

2.setName()/getName

thread.setName(线程名称) //设置线程名称
thread.getName() //返回线程名称

通过设置线程名称,有助于调试程序,提高程序的可读性,建议为每个线程都设置一个能够体现线程功能的名称。

3.isAlive()

thread.isAlive() //判断当前线程是否处于活动状态

4.sleep()

Thread.sleep(millis); //让当前线程休眠指定的毫秒数

实例:计时器(一分钟倒计时)

package se.high.thread;

/**
 * @author 王泽
 * 使用线程休眠,实现一个简单的计数器。
 */

public class SimpleTimer {

    public static void main(String[] args) {
        int remaining = 10 ; //从60秒开始计时

        while(true){
            try {
                System.out.println("时间:  " + remaining);
                if (remaining >= 0){ remaining--; }
                Thread.sleep(1000); //线程休眠
                if(remaining == -1){break;}
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

5.getId() java中的线程都有一个唯一编号

6.yield() 放弃当前的cpu资源

Thread.yield();  //可以让线程由运行转为就绪状态

7.setPriority() 设置线程的优先级

thread.setPriority(num); 设置线程的优先级,取值为1-10,如果超过范围会抛出异常  IllegalArugumentExption;

优先级越高的线程,获得cpu资源的概率越大。
优先级本质上只是给线程调度器一个提示信息,以便于线程调度器决定先调度哪些线程。不能保证优先级高的线程先运行。
java优先级设置不当,可能导致某些线程永远无法得到运行,产生了线程饥饿。
线程的优先级并不是设置的越高越好,在开发时不必设置线程的优先级。

8.interrupt()中断线程 (Thread中的方法。)

因为interrupt()方法只能中断阻塞过程中的线程而不能中断正在运行过程中的线程。

在运行中的线程使用:
注意调用此方法仅仅是在当前线程打一个停止标志,并不是真正的停止线程。
例如在线程1中调用线程b的interrupt(),在b线程中监听b线程的中断标志,来处理结束。
package se.high.thread;

/**
 * @author 王泽
 */

public class YieldTest extends Thread {
    @Override
    public void run() {
        for (int i = 1; i < 1000; i++) {
            // 判断中断标志
            if (this.isInterrupted()){
                //如果为true,结束线程
                //break;
                return;
            }
            System.out.println("thread 1 --->"+i);
        }
    }
}

package se.high.thread;

/**
 * @author 王泽
 */

public class Test {
    public static void main(String[] args) {
        YieldTest t1 = new YieldTest();
        t1.start(); //开启子线程

        //当前线程main线程
        for (int i = 1; i < 100; i++) {
            System.out.println("main --->" + i);
        }
        //打印完main线程中100个后,中断子线程,仅仅是个标记,必须在线程中处理
        t1.interrupt();
    }

}

9.setDaemon() 守护线程

//线程启动前
thread.setDaemon(true);
thread.start();

java中的线程分为用户线程与守护线程

守护线程是为其他线程提供服务的线程,如垃圾回收(GC)就是一个典型的守护线程。

守护线程不能单独运行,当jvm中没有其他用户线程,只有守护线程时,守护线程会自动销毁,jvm会自动退出。

线程的状态

线程的状态:getState()

阶段案例:手写@Async异步注解

思路:通过Aop拦截只要在我们方法上有使用到我们自己定义的异步注解,我们就单独的开启一个异步线程去执行目标方法。

1.自定义一个注解

/**
 * @author 王泽
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyAsync {
    String value() default "";
}

2.Aop编程


 @Aspect 用来类上,代表这个类是一个切面
 @Before 用在方法上代表这个方法是一个前置通知方法
 @After 用在方法上代表这个方法是一个后置通知方法 @Around 用在方法上代表这个方法是一个环绕的方法
 @Around 用在方法上代表这个方法是一个环绕的方法
 */

@Component
@Aspect
@Slf4j
public class ExtThreadAsyncAop {

    @Around(value ="@annotation(org.spring.annotation.MyAsync)")
    public Object around(ProceedingJoinPoint joinPoint){
        try {
            log.info(">环绕通知开始执行<");
            new Thread(new Runnable() {
                @SneakyThrows
                @Override
                public void run() {
                    joinPoint.proceed();//目标方法
                }
            }).start();
            log.info(">环绕通知结束执行<");
            return "环绕通知";
        }catch (Throwable throwable){
            return "系统错误";
        }
    }

}

3.使用自定义注解

@Component
@Slf4j
public class Thread01 {
    @MyAsync
    public void asyncLog(){
        try {
            log.info("目标方法正在执行...阻塞3s");
            Thread.sleep(3000);
            log.info("<2>");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

4.测试

/**
 * @author 王泽
 */
@RestController
@Slf4j
public class Service {
    @Autowired
    private Thread01 thread01;
    @RequestMapping("test")
    public String Test(){
        log.info("<1>");
        thread01.asyncLog();
        log.info("<3>");
        return "test";
    }

}

5.结果:

二、线程安全原理篇

多线程的好处:

  1. 提高了系统的吞吐量,多线程编程可以使一个进程有多个并发(concurrent)。
  2. 提高响应性,服务器会采用一些专门的线程负责用户的请求处理,缩短了用户的等待时间。
  3. 充分利用多核处理器资源,通过多线程可以充分的利用CPU资源。

多线程的问题:

  1. 线程安全问题,多线程共享数据时,如果没有采取正确的并发访问控制措施,就可能产生数据一致性问题,如读取脏数据(过期的数据),如丢失数据更新。

  2. 线程活性(thread liveness)问题。由于程序自身的缺陷或者由资源稀缺性导致的线程一直处于非RUNNABLE状态,这就是线程活性问题。

    常见的线程活性问题:

    1.死锁 (DeadLock) 鹬蚌相争
    2.锁死 (Lockout) 睡美人故事中王子挂啦
    3.活锁 (LiveLock) 类似小猫一直咬自己的尾巴,但是咬不到
    4.饥饿 (Starvation)类似于健壮的雏鸟总是从母鸟嘴中抢食物。

  3. 上下文切换(Context Switch)处理器从执行一个线程切换到执行另外一个线程。

  4. 可靠性可能会由一个线程导致JVM意外终止,其他线程也无法执行。

线程安全问题

什么是线程安全问题?

当多个线程对同一个对象的实例变量,做写(修改)的操作时,可能会受到其他线程的干扰,发生线程安全的问题。

原子性(Atomic):

不可分割,访问(读,写)某个共享变量的时候,从其他线程来看,该操作要么已经执行完毕,要么尚未发生。其他线程看不到当前操作的中间结果。 访问同一组共享变量的原子操作是不能够交错的,如现实生活中从ATM取款。

java中有两种方式实现原子性:
    1.锁 :锁具有排他性,可以保证共享变量某一时刻只能被一个线程访问。
    2.CAS指令 :直接在硬件层次上实现,看做是一个硬件锁。

可见性(visbility):

在多线程环境中,一个线程对某个共享变量更新之后,后续其他的线程可能无法立即读到这个更新的结果。

如果一个线程对共享变量更新之后,后续访问该变量的其他线程可以读到更新的结果,称这个线程对共享变量的更新对其他线程可见。否则称这个线程对共享变量的更新对其他线程不可见。

多线程程序因为可见性问题可能会导致其他线程读取到旧数据(

以上是关于[8w字] | Java多线程全套功法的主要内容,如果未能解决你的问题,请参考以下文章

直接插入排序的进阶功法

java多线程实验 滚动字

iOS多线程全套:线程生命周期,多线程的四种解决方案,线程安全问题,GCD的使用,NSOperation的使用

iOS多线程全套:线程生命周期,多线程的四种解决方案,线程安全问题,GCD的使用,NSOperation的使用(上)

独家干货!腾讯T3-3手写8W字Tomcat体系架构,从性能优化到源码底层

106道Java并发和多线程基础面试题大集合(2w字),这波面试稳了~