多个线程实际运行是走走停停的。线程调度程序会将CPU运行时间划分为若干个时间片段并尽可能均匀的分配给每个线程,拿到时间片的线程被CPU执行这段时间。当超时后线程调度程序会再次分配一个时间片段给一个线程使得CPU执行它。如此反复。由于CPU执行时间在纳秒级别,我们感觉不到切换线程运行的过程。所以微观上走走停停,宏观上感觉一起运行的现象成为并发运行!
多线程的用途:
- 当出现多个代码片段执行顺序有冲突时,希望它们各干各的时就应当放在不同线程上”同时”运行
- 一个线程可以运行,但是多个线程可以更快时,可以使用多线程运行
线程的生命周期图
创建线程方式
继承Thread并重写run方法
一个线程类,重写run方法,在其中定义线程要执行的任务(希望和其他线程并发执行的任务)。启动该线程要调用该线程的start方法,而不是run方法!!!
优点:
-
缺点:
直接继承线程,会导致不能在继承其他类去复用方法,这在实际开发中是非常不便的。
- 定义线程的同时重写了run方法,会导致线程与线程任务绑定在了一起,不利于线程的重用。
```java
public class ThreadDemo1 {
public static void main(String[] args) {
} }//创建两个线程
Thread t1 = new MyThread1();
Thread t2 = new MyThread2();
//启动线程,注意:不要调用run方法!!
t1.start();
t2.start();
class MyThread1 extends Thread{ public void run(){ for (int i=0;i<1000;i++){ System.out.println(“hello姐~”); } } }
class MyThread2 extends Thread{ public void run(){ for (int i=0;i<1000;i++){ System.out.println(“来了~老弟!”); } } }
<a name="Ywpqt"></a>
## 实现Runnable接口单独定义线程任务
(当需要代码复用的时候,优先选择这个)
```java
public class ThreadDemo2 {
public static void main(String[] args) {
//实例化任务
Runnable r1 = new MyRunnable1();
Runnable r2 = new MyRunnable2();
//创建线程并指派任务
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
}
}
class MyRunnable1 implements Runnable{
public void run() {
for (int i=0;i<1000;i++){
System.out.println("你是谁啊?");
}
}
}
class MyRunnable2 implements Runnable{
public void run() {
for (int i=0;i<1000;i++){
System.out.println("开门!查水表的!");
}
}
}
匿名内部类优化代码
public class ThreadDemo3 {
public static void main(String[] args) {
//匿名内部类
Thread t1 = new Thread(){
public void run(){
for(int i=0;i<1000;i++){
System.out.println("你是谁啊?");
}
}
};
//Runnable可以使用lambda表达式创建
Runnable r2 = ()->{
for(int i=0;i<1000;i++){
System.out.println("我是查水表的!");
}
};
Thread t2 = new Thread(r2);
t1.start();
t2.start();
}
}
主线程
1.java中的代码都是靠线程运行的,执行main方法的线程称为”主线程”。 我们自己定义的线程在不指定名字的情况下系统会分配一个名字,格式为”thread-x”(x是一个数)。
2.static Thread currentThread()
该方法可以获取运行这个方法的线程
public class CurrentThreadDemo {
public static void main(String[] args) {
Thread main = Thread.currentThread();//获取执行main方法的线程(主线程)
System.out.println("线程:"+main);
dosome();//主线程执行dosome方法
}
public static void dosome(){
Thread t = Thread.currentThread();//获取执行dosome方法的线程
System.out.println("执行dosome方法的线程是:"+t);
}
}
//线程:Thread[main,5,main]
//执行dosome方法的线程是:Thread[main,5,main]
多线程实现连接
原理图:
多客户端连接一个服务器代码,以及构造方法灵活传参
package socket;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
private ServerSocket serverSocket;
public Server(){
try {
System.out.println("正在启动服务端...");
serverSocket = new ServerSocket(8088);
System.out.println("服务端启动完毕");
} catch (IOException e) {
e.printStackTrace();
}
}
public void start(){
try {
while(true) {
System.out.println("等待客户端连接...");
Socket socket =serverSocket.accept();
System.out.println("一个客户端连接了!");
//启动一个线程来负责与该客户端交互
//1创建线程任务
ClientHandler handler = newClientHandler(socket);
//2创建一个线程并执行该任务
Thread t = newThread(handler);
//3启动该线程
t.start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args){
Server server = new Server();
server.start();
}
//创建一个线程,这里以为要反复调用,所以使用内部类的方式
private class ClientHandler implements Runnable{
private Socket socket;
//采用构造方法进行传参,这样在主线程里面new这个对象的时候,就把参数传进来走下面的run
public ClientHandler(Socket socket){
this.socket = socket;
}
public void run(){
try{
InputStream in =socket.getInputStream();
InputStreamReader isr = newInputStreamReader(in,"UTF-8");
BufferedReader br = newBufferedReader(isr);
String line;
while((line =br.readLine()) != null) {
System.out.println("服务端说:"+line);
}
}catch(IOException e){
e.printStackTrace();
}
}
}
}
线程常用方法
Thread main =Thread.currentThread();//获取主线程
String name = main.getName();//获取线程的名字
System.out.println("名字:"+name);
long id = main.getId();//获取该线程的唯一标识
System.out.println("id:"+id);
int priority =main.getPriority();//获取该线程的优先级
System.out.println("优先级:"+priority);
boolean isAlive = main.isAlive();//该线程是否活着
System.out.println("是否活着:"+isAlive);
boolean isDaemon =main.isDaemon();//是否为守护线程
System.out.println("是否为守护线程:"+isDaemon);
boolean isInterrupted =main.isInterrupted();//是否被中断了
System.out.println("是否被中断了:"+isInterrupted);
setPriority
线程start后会纳入到线程调度器中统一管理,线程只能被动的被分配时间片并发运行,而无法主动索取时间片,线程调度器尽可能均匀的将时间片分配给每个线程,线程有10个优先级,使用整数1-10表示1为最小优先级,10为最高优先级,5为默认值调整线程的优先级可以最大程度的干涉获取时间片的几率,优先级越高的线程获取时间片的次数越多,反之则越少。
min.setPriority(Thread.MIN_PRIORITY);//将其设置为最小优先级
max.setPriority(8);//自定义设置大小
static void sleep(long ms)
线程提供的一个静态方法,使运行该方法的线程进入阻塞状态,持续传参的毫秒,超时后线程会自动回到runnable(可运行的)状态等待再次获取时间片并发运行。
public static void main(String[] args){
System.out.println("程序开始了!");
try {
Thread.sleep(5000);//主线程阻塞5秒钟
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("程序结束了!");
}
InterruptedException(打断异常)
viod interrupt()(打断)
sleep方法处理异常,当一个线程调用sleep方法处于睡眠阻塞的过程中,该线程的interrupt()方法被调用时,sleep方法会抛出该异常从而打断睡眠阻塞.
public static void main(String[] args){
//线程一
Thread lin = new Thread(){
public void run(){
System.out.println("林:刚美完容,睡一会吧...");
try {
Thread.sleep(50000000);
} catch(InterruptedException e) {
System.out.println("林:干嘛呢!干嘛呢!干嘛呢!都破了相了!");
}
System.out.println("林:醒了!");
}
};
//线程二
Thread huang = new Thread(){
public void run(){
System.out.println("黄:大锤80,小锤40.开始砸墙!");
for(int i=0;i<5;i++){
System.out.println("黄:80!");
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
}
}
System.out.println("咣当!");
System.out.println("黄:搞定!");
lin.interrupt();//走到这调用打断方法打断lin
}
};
lin.start();
huang.start();
}//结果会提前终止lin的睡眠阻塞
setDaemon(booleanon)
守护线程也称为:后台线程 守护线程是通过普通线程调用setDaemon(booleanon)方法设置而来的,因此创建上与普通线程无异.守护线程的结束时机上有一点与普通线程不同,即当一个进程中的所有普通线程都结束时,进程就会结束,此时会杀掉所有正在运行的守护线程。所以,当一个进程里只要还有线程在跑,他就继续跑,它不保护任何线程,当所有线程停止,它也同时停止。
jack.setDaemon(true);//将jack设置为守护线程
void join()
- 线程提供了一个方法:该方法允许调用这个方法的线程在该方法所属线程上等待(阻塞),直到该方法所属线程结束后才会解除等待继续后续的工作.所以join方法可以用来协调线程的同步运行。
- 同步运行:多个线程执行过程存在先后顺序进行。
异步运行:多个线程各干各的,线程本来就是异步运行的。
private static boolean isFinish = false;//标示图片是否下载完毕 public static void main(String[] args){ Thread download = new Thread(){ public void run(){ System.out.println("down:开始下载图片..."); for(int i=1;i<=100;i++){ System.out.println("down:"+i+"%"); try { Thread.sleep(50); } catch(InterruptedException e) { } } System.out.println("down:图片下载完毕!"); isFinish = true; } }; Thread show = new Thread(){ public void run(){ try { System.out.println("show:开始显示文字..."); Thread.sleep(3000); System.out.println("show:显示文字完毕!"); //将show线程阻塞,等待download线程执行完毕再继续后续操作 System.out.println("show:等待download下载图片..."); download.join();//show开始阻塞,直到download执行完毕! System.out.println("show:等待完毕!"); System.out.println("show:开始显示图片..."); if(!isFinish){ //当一个线程的run方法抛出一个异常,则线程会结束 throw new RuntimeException("图片加载失败!"); } System.out.println("show:图片显示完毕!"); } catch(InterruptedException e) { e.printStackTrace(); } } }; download.start(); show.start(); } }
多线程并发安全问题
存在问题:
当多个线程并发操作同一临界资源,由于线程切换时机不确定,导致操作临界资源的顺序出现混乱严重时可能导致系统瘫痪。
临界资源:
操作该资源的全过程同时只能被单个线程完成。相当于现实生活中多个人抢同一个东西导致的混乱。比如如下情况,抢到负数还在抢,直接跳过==0的判定了。 ```java public class SyncDemo1 { public static void main(String[] args){
Table table = new Table(); Thread t1 = new Thread(){ public void run(){
while(true){ int bean =table.getBean(); Thread.yield(); System.out.println(getName()+":"+bean); }
} };
Thread t2 = new Thread(){ public void run(){
while(true){ int bean =table.getBean(); Thread.yield(); System.out.println(getName()+":"+bean); }
} };
t1.start(); t2.start(); } }
class Table{ private int beans = 20;//桌子上有20个豆子
public int getBean(){ if(beans==0){ throw new RuntimeException(“没有豆子了!”); } Thread.yield();//让线程主动放弃CPU时间,模拟执行到这里没有时间发生线程切换 return beans—;//会出现都到beans—之前,却都判定进来了,这时候会连续减2次,跳过beans==0 }
}
<a name="kTblf"></a>
## 并发出现问题的根源: 并发三要素
<a name="mr1ay"></a>
### 可见性: CPU缓存引起
> 可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到。看下面这段代码:
```java
//线程1执行的代码
int i = 0;
i = 10;
//线程2执行的代码
j = i;
- 假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。
- 此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10.
:::info
这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。
:::
原子性: 分时复用引起
- 原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
- 经典的转账问题:比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。
- 试想一下,如果这2个操作不具备原子性,会造成什么样的后果。假如从账户A减去1000元之后,操作突然中止。然后又从B取出了500元,取出500元之后,再执行 往账户B加上1000元 的操作。这样就会导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元。
所以这2个操作必须要具备原子性才能保证不出现一些意外的问题。
有序性: 重排序引起
有序性:即程序执行的顺序按照代码的先后顺序执行。举个简单的例子,看下面这段代码: ```java int i = 0;
boolean flag = false; i = 1; //语句1
flag = true; //语句2
```
- 上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗? 不一定,为什么呢? 这里可能会发生指令重排序(Instruction Reorder)。
- 在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:
- 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
- 从 java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:
- 上述的 1 属于编译器重排序,2 和 3 属于处理器重排序。这些重排序都可能会导致多线程程序出现内存可见性问题。对于编译器,JMM 的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM 的处理器重排序规则会要求 java 编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel 称之为 memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。