多线程目的:在同一时刻有多条不同路径执行程序,提高程序运行效率

多线程应用:数据库连接池,多线程文件下载等

注意:在文件下载中使用多线程,无法提高速度

在一个进程中,一定会有主线程

从基础开始,多线程的使用方式:

1.继承Thread类:(不推荐)

public class ThreadDemo extends Thread {
@Override
public void run() {
//写入线程执行的代码
} public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
threadDemo.start();
}
}

注意:threadDemo调用的是start方法;如果调用了run方法,本质上还是单线程

2.实现Runnable接口:

public class ThreadDemo implements Runnable {
@Override
public void run() {
//写入线程执行的代码
System.out.println("demo");
} public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
new Thread(threadDemo).start();
}
}

3.匿名内部类

public class ThreadDemo {
public static void main(String[] args) {
new Thread() {
@Override
public void run() {
//写入线程执行的代码
}
}.start();
}
}

Java8可以简写为这样

public class ThreadDemo {
public static void main(String[] args) {
new Thread(() -> {
//写入线程执行的代码
}).start();
}
}

多线程的状态:

1.新建状态:调用start方法之前

2.就绪状态:调用start方法,等待CPU分配执行权

3.运行状态:执行run方法中的代码

4.死亡状态:run方法执行完毕

5.阻塞状态:调用wait或sleep方法,线程变为阻塞状态,阻塞状态可以直接变成就绪状态

守护线程:

在Java程序中,有主线程和GC线程(用于回收垃圾),主线程死亡后,GC线程也会死亡,同时销毁

这种和主线程一起销毁的线程就是守护线程

非守护线程:线程的状态和主线程无关

用户线程:以上的三种方式创建的都是用户现场,由主线程创建,也是非守护线程

示例:

public class ThreadDemo {
public static void main(String[] args) {
new Thread(() -> {
for (int i = 0; i < 30; i++) {
try {
Thread.sleep(300);
System.out.println("子线程i:" + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程i:" + i);
}
System.out.println("主线程执行完毕");
}
}

观察输出发现:打印主线程执行完毕之后,还在继续打印子线程执行信息

只需要对子线程进行设置,即可变成守护线程:

public class ThreadDemo {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
System.out.println("子线程i:" + i);
}
});
thread.setDaemon(true);
thread.start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程i:" + i);
}
System.out.println("主线程执行完毕");
}
}

观察输出发现:子线程还未打印到999,程序已经结束

join方法:

A线程调用了B线程的join方法,那么A等待B执行完毕之后再执行(A释放CPU执行权)

示例:主线程让子线程执行完毕再执行

public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
for (int i = 0; i < 60; i++) {
System.out.println("子线程i:" + i);
}
});
thread.start();
thread.join();
for (int i = 0; i < 10; i++) {
System.out.println("主线程i:" + i);
}
System.out.println("主线程执行完毕");
}
}

观察输出发现:子线程打印完59,才开始主线程的打印

线程安全问题:

当多个线程共享同一个全局变量,做写的操作时候,会发生线程安全问题

模拟线程安全问题:车站卖票经典案例

public class ThreadDemo implements Runnable {
//一共有一百张票
private int count = 100; @Override
public void run() {
while (count > 0) {
try {
Thread.sleep(100);
sale();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} private void sale() {
if (count > 0) {
System.out.println(Thread.currentThread().getName() + "出售第" + (100 - count + 1) + "张票");
count--;
}
} public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
Thread t1 = new Thread(threadDemo, "窗口1");
Thread t2 = new Thread(threadDemo, "窗口2");
t1.start();
t2.start();
}
}

观察输出发现:很多票重复出售

线程安全问题解决:

1.在sale方法上使用synchronized关键字

原理:当线程进入该方法时候会自动获取锁,一旦某线程获取了锁,其他线程就会等待,等到执行完毕该线程代码,释放锁

缺点:降低程序效率,每次执行该方法都需要进行判断

    private synchronized void sale() {
if (count > 0) {
System.out.println(Thread.currentThread().getName() + "出售第" + (100 - count + 1) + "张票");
count--;
}
}

2.使用同步代码块

public class ThreadDemo implements Runnable {
//一共有一百张票
private int count = 100; private final Object object = new Object(); @Override
public void run() {
while (count > 0) {
try {
Thread.sleep(100);
sale();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} private void sale() {
synchronized (object) {
if (count > 0) {
System.out.println(Thread.currentThread().getName() + "出售第" + (100 - count + 1) + "张票");
count--;
}
}
} public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
Thread t1 = new Thread(threadDemo, "窗口1");
Thread t2 = new Thread(threadDemo, "窗口2");
t1.start();
t2.start();
}
}

观察输出:问题解决

注意:如果写成这样还是存在问题

    public static void main(String[] args) {
ThreadDemo threadDemo1 = new ThreadDemo();
ThreadDemo threadDemo2 = new ThreadDemo();
Thread t1 = new Thread(threadDemo1, "窗口1");
Thread t2 = new Thread(threadDemo2, "窗口2");
t1.start();
t2.start();
}

这时候需要给全局变量加上static关键字:共享同一个锁

    private static int count = 100;

    private static final Object object = new Object();

观察输出:问题解决

多线程死锁问题:

产生场景:初学者喜欢每个地方都加入synchronized,于是synchronized中嵌套synchronized,容易产生死锁

产生原因:A线程拿到了锁2,现在需要拿锁1;B线程拿了锁1,现在需要拿锁2;A线程拿不到锁1就不会释放锁2;B线程拿不到锁2就不会释放锁1

ThreadLocal类:

什么是ThreadLocal:给每一个线程提供局部变量

原理:底层是一个Map集合,获取当前线程,然后调用Map的put和get方法实现

初始化:

    public static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

获取:

        threadLocal.get();

设置:

        threadLocal.set(count);

多线程特性:

1.原子性

2.可见性

3.有序性

Java内存模型(JMM):

JMM决定一个线程对共享变量的写入时,能够另一个线程是否可见

主内存:共享存储的变量

本地内存:共享变量的副本

线程安全问题根本原理:共享变量存放于主内存中,每一个线程都有本地内存。比如我在主内存中存入count=100,那么两个本地内存都存放了count=100副本。这时候两个线程同时操作共享变量count-1,首先两个线程要现在本地内存进行count-1操作,然后刷新到主内存。于是,出现了线程安全问题!

Volatile关键字:

一个示例:

class ThreadTest extends Thread {
public boolean flag = true; @Override
public void run() {
System.out.println("线程开始");
while (flag) { }
System.out.println("线程结束");
} public void setRunning(boolean flag) {
this.flag = flag;
}
} public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
ThreadTest threadTest = new ThreadTest();
threadTest.start();
Thread.sleep(3000);
threadTest.setRunning(false);
System.out.println("flag改为false");
Thread.sleep(3000);
System.out.println("flag:" + threadTest.flag);
}
}

打印如下:

线程开始
flag改为false
flag:false

然后程序卡死

为什么已经把flag改为false,子线程还是走入了while循环

因为:主线程把flag改了,还没有刷入主内存,子线程一直在读本地内存中的变量

解决:只需要加入volatile关键字

作用:将修改的值立即更新到主内存,保证其他线程对该变量的可见

    public volatile boolean flag = true;

打印如下:

线程开始
flag改为false
线程结束
flag:false 

注意:volatile只能保证可见性,不能保证线程安全

使用场景:观察主流框架,可以发现只要是全局共享的变量,都加入了volatile关键字

Synchronized与Volatile关键字区别:

Volatile保证可见性,不能保证原子性,也就是不能保证线程安全,禁止重排序

Synchronized既可以保证原子性,也可以保证线程安全,不禁止重排序

重排序:

概念:CPU会对代码实现优化,不会对有依赖关系性做重排序

什么是依赖关系:

            int a = 1;
int b = 2;
int c = a + b;

c依赖a,b。c和a,b都有关系。c一定在a,b之后执行,而a,b执行顺序不一定

所以在代码执行时候,可能先执行的是int b = 2而不是int a = 1

但是在这里执行的结果不会发生改变

注意:一般只会在多线程中遇到重排序问题

重排序问题的解决:加入volatile关键字

线程之间的通信:

多个线程在处理同一个资源,但是线程的任务却不相同,通过一定的手段使各个线程能有效地利用资源,

这种手段即:等待唤醒机制,又称作线程之间的通信

涉及到的方法:wait(),notify()

示例:

两个线程一个输入,一个输出

package demo;

public class Resource {
public String name;
public String sex;
}

输入线程:

package demo;

public class Input implements Runnable {
private Resource r = new Resource(); public void run() {
int i = 0;
while (true) {
if (i % 2 == 0) {
r.name = "张三";
r.sex = "男";
} else {
r.name = "李四";
r.sex = "女";
}
i++;
}
} }

输出线程:

package demo;

public class Output implements Runnable {
private Resource r = new Resource();
public void run(){
while (true) {
System.out.println(r.name+"..."+r.sex);
}
}
}

测试类:

package demo;

public class ThreadDemo {
public static void main(String[] args) {
Input in = new Input();
Output out = new Output();
Thread tin = new Thread(in);
Thread tout = new Thread(out); tin.start();
tout.start();
}
}

运行后却发现输出的都是null...null

因为输入线程和输出线程中创建的Resource对象使不同的

解决null问题:

package demo;

public class Input implements Runnable {
private Resource r; public Input(Resource r){
this.r = r;
} public void run() {
int i = 0;
while (true) {
if (i % 2 == 0) {
r.name = "张三";
r.sex = "男";
} else {
r.name = "李四";
r.sex = "女";
}
i++;
}
} }
package demo;

public class Output implements Runnable {
private Resource r; public Output(Resource r){
this.r = r;
} public void run(){
while (true) {
System.out.println(r.name+"..."+r.sex);
}
}
}
package demo;

public class ThreadDemo {
public static void main(String[] args) { Resource r = new Resource(); Input in = new Input(r);
Output out = new Output(r);
Thread tin = new Thread(in);
Thread tout = new Thread(out); tin.start();
tout.start();
}
}

运行后又发现了另一个问题:

输出中含有:张三...女或者李四...男,性别出错

发生原因:

赋值完张三和男后,继续赋值李四和女,这时候还未还得及赋值女,就进入了输出线程,这时候就会输出李四...男

于是想到加上同步:

    public void run() {
int i = 0;
while (true) {
synchronized (this) {
if (i % 2 == 0) {
r.name = "张三";
r.sex = "男";
} else {
r.name = "李四";
r.sex = "女";
}
i++;
}
}
}
    public void run() {
while (true) {
synchronized (this) {
System.out.println(r.name + "..." + r.sex);
}
}
}

然而问题并没有解决:

原因:

这里的同步失去了作用,用到的不是一个锁

解决办法:

使用一个共同的锁即可

public void run() {
int i = 0;
while (true) {
synchronized (r) {
if (i % 2 == 0) {
r.name = "张三";
r.sex = "男";
} else {
r.name = "李四";
r.sex = "女";
}
i++;
}
}
}
    public void run() {
while (true) {
synchronized (r) {
System.out.println(r.name + "..." + r.sex);
}
}
}

这时候就是正常的输出了

但是还是存在一个问题,我们希望的是张三和李四交错出现,一个张三一个李四,现在依然是随机出现的,大片的张三或李四

解决办法:

先让input线程赋值,然后让output线程输出,并且让输入线程等待,不允许再赋值李四,等待输出张三结束后,再允许李四赋值,依次下去

输入线程也需要同样的方式,输出完后要等待

这时候就需要用到等待唤醒机制:

输入:赋值后,执行方法wait()永远等待

输出:打印后,再输出等待之前,唤醒输入notify(),自己再wait()永远等待

输入:被唤醒后,重新赋值,必须notify()唤醒输出的线程,自己再wait()等待

依次循环下去

代码实现:

package demo;

public class Resource {
public String name;
public String sex;
public boolean flag = false;
}
package demo;

public class Input implements Runnable {
private Resource r; public Input(Resource r) {
this.r = r;
} public void run() {
int i = 0;
while (true) {
synchronized (r) {
if (r.flag) {
try {
r.wait();
} catch (Exception e) {
}
}
if (i % 2 == 0) {
r.name = "张三";
r.sex = "男";
} else {
r.name = "李四";
r.sex = "女";
}
r.flag = true;
r.notify();
}
i++;
}
}
}
package demo;

public class Output implements Runnable {
private Resource r; public Output(Resource r) {
this.r = r;
} public void run() {
while (true) {
synchronized (r) {
if (!r.flag) {
try {
r.wait();
} catch (Exception e) {
}
}
System.out.println(r.name + "..." + r.sex);
r.flag = false;
r.notify();
}
}
}
}
package demo;

public class ThreadDemo {
public static void main(String[] args) { Resource r = new Resource(); Input in = new Input(r);
Output out = new Output(r);
Thread tin = new Thread(in);
Thread tout = new Thread(out); tin.start();
tout.start();
}
}

这时候就是张三李四交错输出了

完成

Java深入学习(1):多线程的更多相关文章

  1. Java基础学习总结 -- 多线程的实现

    目录: 继承Thread类 start()方法实现多线程的原理 实现Runnable接口 Thread类 与 Runnable接口 的联系与区别 多线程的实现方法: 继承Thread类 实现Runna ...

  2. Java基础学习(八) - 多线程

    理解线程 进程是指一个内存中运行的应用程序,系统运行一个程序即是一个进程从创建,运行,结束的过程. 线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程. 多线程的特点是并发 ...

  3. Java基础学习篇---------多线程

    一.编写两种多线程的方法 (1).Thread(它是继承Runnable的子类) class MyThread extends Thread{ private int ticket = 5; @Ove ...

  4. 吴裕雄--天生自然 JAVA开发学习:多线程编程

    class RunnableDemo implements Runnable { private Thread t; private String threadName; RunnableDemo( ...

  5. 0037 Java学习笔记-多线程-同步代码块、同步方法、同步锁

    什么是同步 在上一篇0036 Java学习笔记-多线程-创建线程的三种方式示例代码中,实现Runnable创建多条线程,输出中的结果中会有错误,比如一张票卖了两次,有的票没卖的情况,因为线程对象被多条 ...

  6. Java学习笔记-多线程-创建线程的方式

    创建线程 创建线程的方式: 继承java.lang.Thread 实现java.lang.Runnable接口 所有的线程对象都是Thead及其子类的实例 每个线程完成一定的任务,其实就是一段顺序执行 ...

  7. Android(java)学习笔记216:多线程断点下载的原理(Android实现)

    之前在Android(java)学习笔记215中,我们从JavaSE的角度去实现了多线程断点下载,下面从Android角度实现这个断点下载: 1.新建一个Android工程: (1)其中我们先实现布局 ...

  8. 2019/3/7 Java学习之多线程(基础)

    Java学习之多线程 讲到线程,就必须要懂得进程,进程是相当于一个程序的开始到结束,而线程是依赖于进程的,没有进程,就没有线程.线程也分主线程和子线程,当在主线程开启子线程时,主线程结束,而子线程还可 ...

  9. Java NIO学习与记录(八): Reactor两种多线程模型的实现

    Reactor两种多线程模型的实现 注:本篇文章例子基于上一篇进行:Java NIO学习与记录(七): Reactor单线程模型的实现 紧接着上篇Reactor单线程模型的例子来,假设Handler的 ...

  10. Android学习记录(5)—在java中学习多线程下载之断点续传②

    在上一节中我们学习了在java中学习多线程下载的基本原理和基本用法,我们并没有讲多线程的断点续传,那么这一节我们就接着上一节来讲断点续传,断点续传的重要性不言而喻,可以不用重复下载,也可以节省时间,实 ...

随机推荐

  1. HDU2650 A math problem——高斯素数

    题意 给你一个数 $a+bj, \ j=\sqrt {-2}$,如果它只能被1.-1.本身和本身的相反数整除,则输出Yes,否则输出No. 分析 高斯整数 $a+bi$ 是素数当且仅当: (1)$a, ...

  2. springCloud学习1

    传统项目架构 传统项目分为三层架构,将业务逻辑层.数据库访问层.控制层放入在一个项目中. 优点:适合于个人或者小团队开发,不适合大团队开发. 分布式项目架构 根据业务需求进行拆分成N个子系统,多个子系 ...

  3. mapreduce 读写Parquet格式数据 Demo

    import java.io.IOException; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs ...

  4. Python面向对象 | 类属性

    property property是一个装饰器函数,可以将一个方法伪装成属性,调用的时候可以不用加().@property被装饰的方法,是不能传参数的,因为它伪装成属性了. 装饰器的使用:在要装饰的函 ...

  5. Pandas | 03 DataFrame 数据帧

    数据帧(DataFrame)是二维数据结构,即数据以行和列的表格方式排列. 数据帧(DataFrame)的功能特点: 潜在的列是不同的类型 大小可变 标记轴(行和列) 可以对行和列执行算术运算 结构体 ...

  6. SFTP 文件上传下载工具类

    SFTPUtils.java import com.jcraft.jsch.*; import com.jcraft.jsch.ChannelSftp.LsEntry; import lombok.e ...

  7. PATA1031 Hello World for U

    参考代码: #include <cstdio> #include <cstring> int main() { char str[100], ans[40][40]; scan ...

  8. ZROI 暑期高端峰会 A班 Day2 线性代数

    高斯消元 很普及组,不讲了 当主元没有逆的时候可以辗转相除. 如果也没有带余数除法--没救了 逆矩阵 我们定义矩阵 \(A\) 的逆矩阵为 \(A^{-1}\),满足 \(AA^{-1}=A^{-1} ...

  9. Spring Boot 知识笔记(整合Mybatis)

    一.pom.xml中添加相关依赖 <!-- 引入starter--> <dependency> <groupId>org.mybatis.spring.boot&l ...

  10. vb.net 获取文件的版本号

    dim strVersion =  Reflection.Assembly.LoadFrom(strFileName).GetName().Version.ToString()