0、介绍


线程:多个任务同时进行,看似多任务同时进行,但实际上一个时间点上我们大脑还是只在做一件事情。程序也是如此,除非多核cpu,不然一个cpu里,在一个时间点里还是只在做一件事,不过速度很快的切换,造成同时进行的错觉。

多线程

方法间调用:普通方法调用,从哪里来到哪里去,是一条闭合的路径;

使用多线程:开辟了多条路径。

进程和线程

也就是 Process 和 Thread ,本质来说,进程作为资源分配的单位,线程是调度和执行的单位。具体来说:

  • 每个进程都有独立的代码和数据空间(进程上下文),进程间切换会有较大开销,操作系统中同时运行多个任务就是进程;
  • 线程可以看成轻量级的线程,同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销较小,同一个应用程序里多个顺序流在执行,他们就是线程,除了CPU外,不会为线程分配内存,它自己使用的是所属进程的资源,线程组只能共享资源。

其他概念

  • 线程可以理解为一个独立的执行路径;
  • 在程序运行的时候,即使没有自己创建线程,后台也会存在gc线程、主线程等,而main() 就是主线程,是程序的入口点;
  • 一个进程里如果开辟了多个线程,线程一旦开始运行,是由调度器安排的,和操作系统紧密相关,他们的安排人为没法干预;
  • 对于同一份资源操作,会涉及资源抢夺问题,需要加入并发控制;
  • 线程会带来cpu调度时间、并发控制等额外的开销;
  • 每个线程只在自己的工作内存交互,如果加载和存储主内存控制不当,就会造成数据不一致,也就是线程不安全。

创建线程

在 java 中,创建线程有 3 种方式:

  1. 继承Thread类(重写run方法)
  2. 实现Runnable接口(重写run方法)
  3. 实现Callable接口(重写call方法,这个是在j.u.c包下的)

根据设计原则,不管是里氏替换原则,还是在工厂设计模式种,都提到过,尽量多用实现,少用继承,所以一般情况下尽量使用第二种方法创建线程。


一、创建方法1:继承Thread类


先直接看下面一个 demo

/*
创建方式1:继承Thread + 重写run
启动方式:创建子类对象 + start
*/
public class StartThread extends Thread {
//线程入口点
@Override
public void run() {
for (int i=0; i<50; i++){
System.out.print("睡觉ing ");
}
} public static void main(String[] args) {
//创建子类对象
StartThread startThread = new StartThread();
//启动,主意是start
startThread.start();
for (int i=0; i<50; i++){
System.out.print("吃饭ing ");
}
}
}

我们把上面的run方法成为线程的入口点,里面是线程执行的代码,当程序运行之后,可以发现,每次的运行结果都是不一样的。

可以看到这种随机穿插执行的结果,这是由cpu去安排时间片,调度决定的

到这里我们总结使用第一种方法创建线程的步骤就是:

  1. 创建子类对象,这个子类是继承了Thread类的;
  2. 启动,调用start方法,而不是run方法,start方法是把这个线程丢给cpu的调度器,让他适时运行而不是立即运行。如果使用run方法,那么就是单纯的执行,并没有开启多线程,会先执行完上面的内容,再往下走。


二、创建方法2:实现Runnable接口


这种方法是推荐的方式,和上一种写法相比较,很简单,只需要把 extends Thread 改成 implements Runnable ,其他的地方几乎没有变化。

区别在于,调用的时候,不能直接 start(),只能借助一个 Thread 对象作为代理。

/*
创建方式2:实现Runnable + 重写run
启动方式:创建实现类对象 + 借助thread代理类 + start
*/
public class StartThreadwithR implements Runnable {
@Override
public void run() {
for (int i=0; i<50; i++){
System.out.print("睡觉ing ");
}
} public static void main(String[] args) {
StartThreadwithR startThread = new StartThreadwithR();
//创建代理类
Thread t = new Thread(startThread);
t.start();//启动
for (int i=0; i<50; i++){
System.out.print("吃饭ing ");
}
}
}

总结第二种创建线程的方法步骤是:

  1. 创建实现类对象,实现类实现的是Runnable接口;
  2. 创建代理类Thread
  3. 将实现类对象丢给代理类,然后用代理类start。

特殊的,如果我们的一个对象只使用一次,那就完全可以用匿名,上面的

        StartThreadwithR startThread = new StartThreadwithR();
Thread t = new Thread(startThread);
t.start();

可以改成:

new Thread(new StartThreadwithR()).start();

两种方法相比,因为推荐优先实现接口,而不是继承类,所以第二种方法是推荐的。


三、可能出现的问题


3.1 黄牛订票

当多个线程同时进行修改资源的时候,可能出现线程不安全的问题,最上面我们提到了,这里做一个简单模拟。

假如三个黄牛同时在抢票,服务端的票数--的过程,对于三个线程可能会出现哪些问题呢?

/*
使用多线程修改资源带来的线程安全问题
*/
public class Tickets implements Runnable{
private int ticketNum = 100;
@Override
public void run() {
while(true){
if (ticketNum<0){
break;
}
System.out.println(Thread.currentThread().getName() + "正在抢票,余票" + ticketNum--);
}
}
//客户端
public static void main(String[] args) {
Tickets tickets = new Tickets();
//多个Thread代理
new Thread(tickets,"黄牛1").start();
new Thread(tickets,"黄牛2").start();
new Thread(tickets,"黄牛3").start();
}
}

这里面用了简单的模拟服务端和客户端行为,请求票的时候,分别对票数进行 -- 操作,执行之后我们来看:

显然出现了逻辑上的错误,因为多个线程的执行带来的问题。

从运行结果的最后两行入手,背后的原因是:

  1. 黄牛 2 先进入run;
  2. 可是到将票数-1之前,由于cpu的调度,黄牛 3 线程也开始执行,并且比黄牛 2 更快一步,直接进行了 -- 操作,票数变成了 0 ;
  3. 此时黄牛 2 输出了结果,余票0;
  4. 随后黄牛 3 线程才执行完输出语句,票数反倒是 1 ?

如果我们再模拟一个网络延迟,在 run 方法里加入:

//加入线程阻塞,模拟网络延迟
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}

多运行几遍,甚至可能票数变成负数。

显然,如果在实际开发中,票数的变化,应该是严格递减的过程,并且,余票到达 0 就应该 break,而不能还出现继续执行了--操作,从而出现这种错误(不考虑退票之类的业务)。

这就是 高并发 问题,主要就是多线程带来的安全问题。


3.2 龟兔赛跑

再来看一个例子,假如有乌龟和兔子进行赛跑,我们模拟两个线程,分别对距离++。

/*
龟兔赛跑,借助Runnable和Thread代理
*/
public class Racer implements Runnable{
private String winner;
@Override
public void run() {
for (int dis=1; dis<=100; dis++){
System.out.println(Thread.currentThread().getName() + " 跑了 " + dis);
//每走一步,判断是否比赛结束
if (gameOver(dis))break;
}
} public boolean gameOver(int dis){
if (winner != null){
return true;
} else if (dis == 100){
winner = Thread.currentThread().getName();
System.out.println("获胜者是 "+winner);
return true;
}
return false;
} public static void main(String[] args) {
Racer racer = new Racer();//1.创建实现类
new Thread(racer,"兔子").start();//2.创建代理类并start
new Thread(racer,"乌龟").start();
}
}

这样运行起来,总会有一个人赢,但是赢的每次不一定是哪一个。


四、创建方法3:实现Callable


面对高并发的情况,需要用到线程池。

来看重新实现的龟兔赛跑:

/*
创建方法3:Callable,是java.util.concurrent包里的内容
*/
public class RacerwithCal implements Callable<Integer> {
private String winner; //需要实现的是call方法
@Override
public Integer call() throws Exception {
for (int dis=1; dis<=100; dis++){
System.out.println(Thread.currentThread().getName() + " 跑了 " + dis);
//每走一步,判断是否比赛结束,并且结束可以有返回值
if (gameOver(dis))return dis;
}
return null;
} public boolean gameOver(int dis){
if (winner != null){
return true;
} else if (dis == 100){
winner = Thread.currentThread().getName();
if (winner.equals("pool-1-thread-1"))System.out.println("获胜者是 乌龟");
else System.out.println("获胜者是 兔子");
return true;
}
return false;
} public static void main(String[] args) throws ExecutionException, InterruptedException {
//1.创建目标对象
RacerwithCal race = new RacerwithCal();
//2.创建执行服务,含有2个线程的线程池
ExecutorService service = Executors.newFixedThreadPool(2);
//3.提交执行
Future<Integer> result1 = service.submit(race);
Future<Integer> result2 = service.submit(race);
//4.获取结果:pool-1-thread-1也就是第一个线程是乌龟,第二个兔子
Integer i = result1.get();
Integer j = result2.get();
System.out.println("比分是: "+ i + " : " + j);
//5.关闭服务
service.shutdownNow();
}
}

来看执行结果:

总结一下,步骤一般分为 5 步:

  1. 创建目标对象;
  2. 创建执行服务;
  3. 提交执行;
  4. 获取结果;
  5. 关闭服务。

可以看到,这种方法的特殊之处在于:

  • 目标类继实现Callable接口的 call 方法,可以有返回值(前面的run是没有返回值的);
  • 不用处理异常,可以直接 throw;
  • 使用的过程相比前两种方法,变得复杂。


五、静态代理模式


注意到在前面使用第二种方法创建多线程的时候,提到了 new Thread(tickets,"黄牛1").start(); 是使用了 Thread 作为代理。代理模式本身也是设计模式种的一种,分为动态代理和静态代理,代理模式在开发中记录日志等等很常用。

静态代理的代理类是直接写好的,拿过来用,动态代理则是在程序执行过程中临时创建的。

在这里简单介绍静态代理。

实现一个婚庆公司,作为你的婚礼的代理,然后进行婚礼举办。

/*
静态代理模式demo
1.真实角色
2.代理角色
3.1和2都实现同一个接口
*/
public class StaticProxy {
public static void main(String[] args) {
//完全类似于 new Thread(new XXX()).start();
new WeddingCompany(new You()).wedding();
}
} //接口
interface Marry{
void wedding();
} //真实角色
class You implements Marry{
@Override
public void wedding() {
System.out.println("结婚路上ing");
}
} //代理角色
class WeddingCompany implements Marry{
//要代理的真实角色
private Marry target;
public WeddingCompany(Marry target) {
this.target = target;
} @Override
public void wedding() {
ready();//准备
this.target.wedding();
after();//善后
}
private void after() {
System.out.println("结束ing");
}
private void ready() {
System.out.println("布置ing");
}
}

可以看到,最后的调用方法就相当于是写线程的时候用到的 new Thread(new XXX()).start();

小小区别就在于,我们写的线程类是实现的 run 方法,没有实现start方法,但是不重要。

重要的是,代理类 可能做了很多的事,而中间需要 真实类 实现的一个方法必须实现,其他的方法,真实类不需要关心,也就是交给代理类去办了。


六、Lambda表达式简化线程


jdk1.8 后可以使用 lambda 表达式来简化代码,一般用在 只使用一次的、简单的线程 里面。

简化的写法有很多,下面是逐渐简化的过程。


6.1 静态内部类

如果某个类只希望使用一次,可以用静态内部类来实现,调用的时候一样。

public class StartThreadLambda {
//静态内部类
static class Inner implements Runnable{
@Override
public void run() {
for (int i=0; i<50; i++){
System.out.print("睡觉ing ");
}
}
}
//静态内部类
static class Inner2 implements Runnable{
@Override
public void run() {
for (int i=0; i<50; i++){
System.out.print("吃饭ing ");
}
}
}
public static void main(String[] args) {
new Thread(new Inner()).start();
new Thread(new Inner2()).start();
}
}

使用静态内部类的好处是,不使用的时候这个内部类是不会编译的,这其实就是一个单例模式。


6.2 方法内部类

还可以直接写到 main 方法内部,因为main 方法就是static,只启动一次。

public class StartThreadLambda {
public static void main(String[] args) {
//方法内部类(局部内部类)
class Inner implements Runnable{
//。。。。。。
}
class Inner2 implements Runnable{
//。。。。。。
}
new Thread(new Inner()).start();
new Thread(new Inner2()).start();
}
}


6.3 匿名内部类

更进一步,可以直接利用匿名内部类,不用声明出类的名称来。

public class StartThreadLambda {
public static void main(String[] args) {
//匿名内部类,必须借助接口或者父类,因为没有名字
new Thread(new Runnable() {
@Override
public void run() {
for (int i=0; i<50; i++){
System.out.print("吃饭ing ");
}
}
}).start(); new Thread(new Runnable() {
@Override
public void run() {
for (int i=0; i<50; i++){
System.out.print("睡觉ing ");
}
}
}).start();
}
}

这里面必须带上实现体了就,因为没有名字,那么就要借助父类或者接口,而父类或者接口的run方法是需要重写/实现的。


6.4 Lambda表达式

jdk 8 对匿名内部类写法再进行简化,只用关注线程体,也就是只关注 run 方法里面的内容。

public class StartThreadLambda {
public static void main(String[] args) {
//使用Lambda表达式
new Thread(()-> {
for (int i=0; i<50; i++){
System.out.print("吃饭ing ");
}
}).start(); new Thread(()->{
for (int i=0; i<50; i++){
System.out.print("睡觉ing ");
}
}).start();
}
}

() - > 这个符号,编译器就默认你是在实现 Runnable,并且默认是在实现 run 方法。


6.5 扩展

显然,如果不是线程,是其他的我们自己写的接口+实现类,Lambda表达式也是可用的,而且可以进行参数和返回值的扩展。

public class LambdaTest {
public static void main(String[] args) {
//直接使用lambda表达式实现接口
Origin o = (int a, int b)-> {
return a+b;
};
System.out.println(o.sum(100,100));
}
} //自定义接口,相当于Runnable
interface Origin{
int sum(int a, int b);
}

更有甚者,参数的类型也可以省略,他会自己去匹配:

//省略参数类型
Origin o1 = (a, b) -> {
return a+b;
};

如果实现接口的方法,只有一行代码,甚至花括号也可以省略:

Origin o2 = (a, b) -> a+b;

有关返回值和参数的个数还是有一些细微差别的。

Lambda表达式也在 Sort 方法里有应用,要想对引用类型里面统一按照某个属性进行排序,需要实现Comparator接口里面的compare方法,可以使用简化写法。

  • Lambda 表达式的支持,主要是为了避免匿名内部类定义过多,实质上是属于函数式编程的概念
  • 需要注意的是,Lambda表达式只支持实现一个方法。

java的线程、创建线程的 3 种方式、静态代理模式、Lambda表达式简化线程的更多相关文章

  1. 多线程的创建,并发,静态代理,Lambda表达式

    程序是指令和数据的有序集合,本身没有任何运行的含义.是一个静态的概念. 在操作系统中运行的程序就是进程(Process),如:QQ,播放器,游戏等等. 进程是程序的一次执行过程,是一个动态的概念,是系 ...

  2. Java 8 创建 Stream 的 10 种方式,我保证你受益无穷!

    之前栈长分享过 Java 8 一系列新特性的文章,其中重点介绍了 Stream. 获取上面这份 Java 8~12 系列新特性干货文章,请在微信搜索关注微信公众号:Java技术栈,在公众号后台回复:j ...

  3. Java并发编程:线程间协作的两种方式:wait、notify、notifyAll和Condition

    Java并发编程:线程间协作的两种方式:wait.notify.notifyAll和Condition 在前面我们将了很多关于同步的问题,然而在现实中,需要线程之间的协作.比如说最经典的生产者-消费者 ...

  4. 19、Java并发编程:线程间协作的两种方式:wait、notify、notifyAll和Condition

    Java并发编程:线程间协作的两种方式:wait.notify.notifyAll和Condition 在前面我们将了很多关于同步的问题,然而在现实中,需要线程之间的协作.比如说最经典的生产者-消费者 ...

  5. 多线程之线程间协作的两种方式:wait、notify、notifyAll和Condition

    Java并发编程:线程间协作的两种方式:wait.notify.notifyAll和Condition 在前面我们将了很多关于同步的问题,然而在现实中,需要线程之间的协作.比如说最经典的生产者-消费者 ...

  6. Linux线程间同步的几种方式

    信号量 信号量强调的是线程(或进程)间的同步:"信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作(大家都在sem_wait的时候,就阻塞 ...

  7. Java创建Timestamp的几种方式

    1.java创建Timestamp的几种方式 Timestamp time1 = new Timestamp(System.currentTimeMillis()); Timestamp time2 ...

  8. JAVA SparkSQL初始和创建DataFrame的几种方式

    建议参考SparkSQL官方文档:http://spark.apache.org/docs/latest/sql-programming-guide.html 一.前述       1.SparkSQ ...

  9. Spring创建JobDetail的两种方式

    一.Spring创建JobDetail的两种方式 二.整合方式一示例步骤 1.将spring核心jar包.quartz.jar和Spring-context-support.jar导入类路径. 2.编 ...

随机推荐

  1. P1330 封锁阳光大学(洛谷)

    题目描述 曹是一只爱刷街的老曹,暑假期间,他每天都欢快地在阳光大学的校园里刷街.河蟹看到欢快的曹,感到不爽.河蟹决定封锁阳光大学,不让曹刷街. 阳光大学的校园是一张由n个点构成的无向图,n个点由m条道 ...

  2. javascript : 复杂数据结构拷贝实验

    数组深拷贝看起来很简单. array.concat()就行了. 但是,如果数组里有对象呢? 实际上,你以为你拷贝了对象,但实际上你只拷贝了对象的引用(指针)! 我们可以做个试验. // test le ...

  3. 关于Type-C扩展坞干扰路由器交换机的解决方案

    近期看到网友反馈Type-C扩展坞干扰交换机的问题,具体表现为USB Type-C扩展坞在同时插上网线和PD充电的情况下,引起路由器或交换机死机,导致局域网断开的情况.经实测分析,原因为部分电脑默认设 ...

  4. Pyramid attention networks for image restoration

    paper:https://arxiv.org/abs/2004.13824 code: https://github.com/SHI-Labs/Pyramid-Attention-Networks ...

  5. socket网络

    Socket 是进程间通信的一种方式,它与其他进程间通信的一个主要不同是:它能实现不同主机间的进程间通信,我们网络上各种各样的服务大多都是基于 Socket 来完成通信的,例如我们每天浏览网页.QQ ...

  6. MacOS下SpringBoot基础学习

    学于黑马和传智播客联合做的教学项目 感谢 黑马官网 传智播客官网 微信搜索"艺术行者",关注并回复关键词"springboot"获取视频和教程资料! b站在线视 ...

  7. 00_01_使用Parallels Desktop创建WindosXP虚拟机

    打开paralles软件,选择文件->新建 继续 选择手动选择,之后勾选没有指定源也继续 选择要创建的操作系统(这里以XP为例,其他的windows系统安装基本都差不多) 根据需要选择,这里选择 ...

  8. UDP 网络程序-发送_接收数据

    """ 创建udp连接 发送数据给 """ from socket import * # 创建udp套接字,使用SOCK_DGRAM udp ...

  9. 2020牛客暑假多校训练营 第二场 G Greater and Greater bitset

    LINK:Greater and Greater 确实没能想到做法. 考虑利用bitset解决问题. 做法是:逐位判断每一位是否合法 第一位 就是 bitset上所有大于\(b_1\)的位置 置为1. ...

  10. CF EC 87 div2 1354 C2 Not So Simple Polygon Embedding 计算几何 结论

    LINK:Not So Simple Polygon Embedding 搞了好久终于搞会了. 错误原因 没找到合适算边长的方法 要么就是边长算的时候算错了. 几何学的太差了 最后虽然把十边形的和六边 ...