一、基本介绍
什么是线程?
线程是由进程创建的,是进程的一个实体。
举个例子:
当我们使用迅雷的时候,迅雷就是一个进程,而当你每下载一部电影的时候,就会开启一个线程。
什么是进程?
进程就是指运行中的程序,如点击迅雷,就启动了一个进程,操作系统就会为迅雷分配内存空间。
![](https://img-blog.csdnimg.cn/img_convert/9eb1953de127c99691e0ff1dae6c3e74.png)
线程相关概念:
并发:同一时刻,单核cpu(某一个cpu)在对多个任务进行交替执行。
并行:同一时刻,多核cpu(多个cpu)在对多个任务进行同时执行
注意:并发是对于一个cpu,并行则是对于多个cpu,在使用中并发和并行可同时存在
案例引入
在实际中,我们为了提高效率,可以使用多线程来处理事情,即同时处理多个任务。
举个大脑多线程的使用例子:
左手画圆,有手画矩形
那么在java中又如何使用多线程呢?
二、创建线程
通过继承Thread类
class A extends Thread{
}
此时我们还需要重写一个run()方法!
class A extends Thread{
@Override
public void run() {
}
}
这样我们就具备了创建线程的条件了。下面我们创建线程
public class test01 {
public static void main(String[] args) {
A a = new A();
a.start(); //开启线程
}
}
class A extends Thread{
@Override
public void run() {
}
}
注意:一定要在main()方法中通过start()方法,才能真正创建一个线程,如果直接调用run()方法是实现不了的(具体原因下面说明)。
通过实现Runnable接口
当然我们也要重写run()方法
class A implements Runnable{
@Override
public void run() {
}
}
但是创建线程的方法就不一样了,因为Runnable这个接口他就一个run()方法,没有start()方法。
![](https://img-blog.csdnimg.cn/img_convert/8740995efe45e58f115c888da6b31d94.png)
聪明java设计者于是就重载了一个Thread类的构造方法。
![](https://img-blog.csdnimg.cn/img_convert/4a5145d3bc5527accf28ad66a9fa534e.png)
于是乎,我们现在可以通过创建一个Thread类对象,通过多态,来调用start()方法。
public class test01 {
public static void main(String[] args) {
A a = new A();
Thread thread = new Thread(a);
thread.start(); //我又创建出来拉~
}
}
class A implements Runnable{
@Override
public void run() {
}
}
两种实现方式的对比
通过继承Thread的方式,可以直接调用Thread类中方法
通过实现Runnable接口的方式,更加灵活,因为java只有单继承,这样你就可以继承其他重要的类辣。
思考:为什么不直接调用run()方法呢?
run()方法的本质就是一个普通的方法,没有真正的启动一个线程。
start()方法,他会去调用start0()方法,start0()方法是由JVM来调用的,内部源码看不到,不过肯定调用了run()方法,start0()的底层c/c++实现,就不再深究了,切勿走火入魔~
![](https://img-blog.csdnimg.cn/img_convert/273be33bb1200d0041a4d4ce5a56130c.png)
![](https://img-blog.csdnimg.cn/img_convert/170f10959b408ae4739126ecd3c326d3.png)
因此,真正实现多线程的是start0()方法,而不是run()方法。
浅析线程创建
为了常看输出结果,我们每输出一次就让他睡1s
public class test01 {
public static void main(String[] args) throws InterruptedException {
A a = new A();
Thread thread = new Thread(a);
thread.start();
for(int i = 0; i < 50; i++){
System.out.println("main线程在运行~");
Thread.sleep(1000);
}
}
}
class A implements Runnable{
@Override
public void run() {
while(true){
System.out.println("线程A在运行~");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
创建示意图:
![](https://img-blog.csdnimg.cn/img_convert/6c74ff6447c1f1e3112cef9eb89bb228.png)
当我们对当前程序进行运行(run)的时候
![](https://img-blog.csdnimg.cn/img_convert/82e568ae6f3e5c0b715fe7ca224ab96d.png)
首先开了一个进程,然后启动了一个main线程,在main线程中又开启了一个子线程-A线程。
注意:
当main线程执行完结束了,子线程并不会直接结束,进程也不会结束,而是等待其他线程都结束了,进程才会结束。当main线程启动了子线程,main线程不会不动(阻塞),而是继续执行。
所以最终结果如下(线程A是死循环打印):
![](https://img-blog.csdnimg.cn/img_convert/e88d055f8a5bfe4b3935d4754e6a0010.png)
线程终止
等线程完成任务,自动退出
通过修改线程某个变量,来控制run()方法,让他提前退出
案例:
public class test01 {
public static void main(String[] args) throws InterruptedException {
A a = new A();
Thread thread = new Thread(a);
thread.start();
Thread.sleep(2*1000);
//通知线程终止
A.setLoop();
}
}
class A implements Runnable{
private static boolean loop = true;
@Override
public void run() {
while(loop){
System.out.println("线程A在运行~");
}
}
public static void setLoop(){
loop = false;
}
}
三、线程常用方法
常用方法
1.start() | 创建并执行该线程,底层是JVM调用该线程的start0()方法 |
2.run() | 调用当前线程的run()方法 |
3.setName(Stringname) | 设置线程的名称为name |
4.getName() | 获取线程的名称 |
5.sleep(long millis) | 让线程休眠millis毫秒(但不会终止线程!) |
6.interrupt() | 中断线程 |
7.yield() | 线程礼让,让出cpu,让其他线程执行,但是会根据实际情况来决定,礼让是否会成功(取决于当时cpu负载) |
8.join() | 线程插队。如果线程插队成功,则必须先执行完插入的线程中所有任务,才进行其他线程 |
守护线程
守护线程一般是为工作线程服务的,当所有的用户线程结束后,守护线程自动结束。
举个例子:
通过使用多线程,我们知道main线程结束后,创建的子线程并不会结束,如果此时我们想要让子线程结束该怎么办呢?
通过使用线程终止的方式,手动让线程终止
将线程设置为守护线程
如下:
public class test01 {
public static void main(String[] args) throws InterruptedException {
A a = new A();
Thread thread = new Thread(a);
thread.setDaemon(true); //将线程设置为守护线程
thread.start();
}
}
class A implements Runnable{
@Override
public void run() {
while(true){
System.out.println("线程A在运行~");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
注意:设置守护线程的语句要先与创建的语句,当所有用户线程结束后,守护线程才会结束
thread.setDaemon(true); //一定要先于start(),不然会抛异常
thread.start();
四、线程的状态
JDK中用Thread中的State枚举类表示了6种线程的状态
NEW | 尚未启动的线程处于此状态 |
RUNNABLE | 在JAVA虚拟机中执行的线程处于此状态 |
BLOCKED | 被阻塞等待监视器锁定的线程处于此状态 |
WAITING | 正在等待另一个线程执行的线程处于此状态 |
TIMED_WAITING | 正在等待另一个线程执行到指定等待时间的线程处于此状态 |
TERMINATED | 已退出的线程处于此状态 |
如下代码可以看到除BOLCKED的状态:
public class test01 {
public static void main(String[] args) throws InterruptedException {
A a = new A();
System.out.println("线程A的状态:" + a.getState());
a.start();
for(int i = 0; i <5; i++){
System.out.println("线程A的状态:" + a.getState());
Thread.sleep(1000);
}
Thread.sleep(10* 1000);
System.out.println("线程A的状态:" + a.getState());
}
}
class A extends Thread{
@Override
public void run() {
B b = new B();
b.start();
try {
b.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
for(int i = 0; i < 5; i++){
System.out.println("线程A正在执行~");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
class B extends Thread{
@Override
public void run() {
try {
sleep(2*1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
![](https://img-blog.csdnimg.cn/img_convert/7ccea4947c288614c76ca9f96b18fa17.png)
五、线程同步机制
为什么要引入这个东西呢?
案例引入
现模拟一个学生选课系统,有多个学生来抢一门课。
public class test02 {
public static void main(String[] args) {
//模拟三个学生来抢
GetClass getClass = new GetClass();
new Thread(getClass).start();
new Thread(getClass).start();
new Thread(getClass).start();
}
}
class GetClass implements Runnable{
private int classnums = 20;
@Override
public void run() {
while(true){
if(classnums <= 0){
System.out.println("课被抢完了~");
break;
}
else{
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("恭喜"+ Thread.currentThread().getName() + "抢到了,"+ "现在还剩" + (--classnums) + "名额");
}
}
}
}
输出结果如下:
![](https://img-blog.csdnimg.cn/img_convert/bd41abe8c44a63f2a6fbd9282937ccb8.png)
我们会发现不仅有超名额,还有重复抢一个名额。
这是因为某一时刻,一下进去了好几个线程,同时抢,同时减~
所以我们就引入了线程同步机制(锁)。
基本介绍
在多线程编程时,有一些敏感数据不允许被多个线程同时访问,可以使用线程同步机制,保证在任何时刻,数据只能有一个线程访问,即当有一个线程在对内存操作时,其他线程都不能对这个内存操作。
基本用法
使用代码块
synchronized (对象){
//需要被同步的代码
}
声明在方法中,表示整个方法同步
public synchronized void fun(){
//需要被同步的代码
}
通过线程同步机制,就能很好的解决案例中出现的问题。
public class test02 {
public static void main(String[] args) {
//模拟三个学生来抢
GetClass getClass = new GetClass();
new Thread(getClass).start();
new Thread(getClass).start();
new Thread(getClass).start();
}
}
class GetClass implements Runnable{
private int classnums = 20;
@Override
public void run() {
while(true){
synchronized (this){
if(classnums <= 0){
System.out.println("课被抢完了~");
break;
} else{
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("恭喜"+ Thread.currentThread().getName() + "抢到了,"+ "现在还剩" + (--classnums) + "名额");
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
输出如下:
![](https://img-blog.csdnimg.cn/img_convert/1f842e2623193d63a281a83758d36a71.png)
一点毛病都没有了~
Synchronized细节
1.在java中引入了对象互斥锁的概念,目的是为了保证共享数据的完整性,但会降低效率。
2.每个对象都都有一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问这个对象。
3.当某个对象被synchronized修饰的时候,一个线程访问了这个对象,那么它就有了这个互斥锁,即相当于拥有了这个对象的标记,此时该对象只能在同一时刻被一个对象所访问。
4.synchronized修饰非静态方法,锁的对象可以是this或其他对象
public synchronized void fun(){
}
5.synchronized修饰静态方法,锁的对象是当前类本身
class A extends Thread{
public static void fun(){
synchronized (A.class){
}
}
}
注意:
synchronized修饰普通方法,默认锁对象为this
synchronized修饰静态方法,默认锁对象为当前类
多线程锁的对象一定为同一个,否则锁了没用
死锁
举个例子:
小明对小王说:你先请我,我就请你。
小王对小明说:你先请我,我就请你。
于是就陷入死循环了。
创建一个死锁:
public class test01 {
public static void main(String[] args) throws InterruptedException {
DeadLockDemo deadLockDemo = new DeadLockDemo(true);
DeadLockDemo deadLockDemo1 = new DeadLockDemo(false);
deadLockDemo.start();
deadLockDemo1.start();
}
}
class DeadLockDemo extends Thread {
static Object o1 = new Object();
static Object o2 = new Object();
boolean flag;
public DeadLockDemo(boolean flag){
this.flag = flag;
}
@Override
public void run() {
if(flag){
synchronized (o1){
System.out.println("111");
synchronized (o2){
System.out.println("222");
}
}
}else{
synchronized (o2){
System.out.println("333");
synchronized (o1){
System.out.println("444");
}
}
}
}
}
分析:一个线程想要获得o1对象的,才能执行下去,另一个线程想要获得o2对象的锁才能执行下去,两个线程都想要获得对象手中的锁,因此程序会卡在那。
![](https://img-blog.csdnimg.cn/img_convert/055064aec523ff5af70b1a09cc1b250b.png)
所以一定要避免这种情况的发生。
最后
本人水平有限,如有问题,欢迎指出~