0%

Java学习基础-多线程

一.多线程
1.1 并行和并发
并行:两个事件,在同一时刻,都在发生
并发:两个事件,在同一时间段内,都在发生

1.2 进程和线程
进程:正在内存中运行的程序,称为进程
•是系统进行资源分配和调用的独立单位
•每一个进程都有自己的诶次空间和系统资源

线程:是进程中的单个顺序控制流,进程中完成某个小功能的模块(进程中用于执行某个功能的执行单元)
•单线程:一个进程如果只有一条执行路径,则称为单线程程序
•多线程:一个进程如果有多条执行路径,则称为多线程程序

•进程和线程的区别
线程是属于某个进程的
进程:每个进程都有自己独立的内存空间(栈/堆),并且至少有一个线程
线程:每个线程都会跟所在的进程申请一块独立的栈,堆共享进程的堆(堆是共享的,栈是独立的),线程小号的资源比进程小得多

•线程调度
线程调用是指CPU在不同的进程不同的线程之间进行快速切换

线程调度的分类:
分时调度:每个线程平均拥有CPU的执行权
抢占式调度:每个线程随机分配CPU的执行权(具体的分配多少和线程的优先级有关)
Java进程中,所有的线程,采用抢占式调度

1.3 Thread类的介绍
例:IDE的main方法运行时是创建一个进程,该进程中至少有2个线程
I.main方法所在的线程,称为主线程
II.垃圾回收期线程

a.Thread类是什么
Thread是Java定义好的,代表线程的类,只要创建该类的一个对象,其实介绍创建了一个线程

b.Thread类的构造方法
public Thread();//无参构造
public Thread(String name);//带有线程名的构造
public Thread(Runnable r);//带有线程任务的构造
public Thread(Runnable r,String name);//即带有线程名字,又有线程任务的构造

c.Thread类的成员方法
public String getName();//获取线程的名字。无参构造,线程会有自己的名字,如Thread0,Thread1
public String setName(String name);//修改线程的名字
public void run();//代表线程要执行的任务,任务有关的代码需要写在此方法中
public void start();//线程只创建并不会执行,必须调用start开启后才会执行任务

public static void sleep(long millis);//让当前线程休眠X毫秒(指Thread.sleep(1000)代码写在哪个线程中,所在的线程就当前线程)
public static Thread currentThread();//获取当前线程对象(指Thread.currentThread()代码写在哪个线程中,所在的线程就当前线程)

public static void join();//等待这个线程死亡
public static void setDaemon(boolean on);//将此线程标记为守护线程,当允许的线程都是守护线程时,Java虚拟机将退出

public final int getPriority();//返回此线程的优先级
public final void setPriority(int newPriority);//更改此线程的优先级
线程优先级高,仅表示获取CPU时间片的几率高,并不表示会优先执行

1.4 创建新的线程方式一:继承方式
a. API描述:将类声明为Thread的子类。该子类重写Thread类的run方法。接下来可以分配并启动该子类的实例
b.分析创建步骤:
I.创建子类 继承 Thread
II.子类中重写run方法(在run中编写线程要执行的任务代码)

•为什么要重写run()方法:因为run()是用来封装被线程执行的代码
•run()方法和start()方法的区别:
i.run()封装线程执行的代码,直接调用,相当于普通方法的调用
ii.start()启动线程,然后由JVM调用此线程的run()方法

III.创建子类对象(实际上就是创建一个线程对象)
IV.调用线程对象的start方法(启动该线程)
案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyThread extends Thread{
@Override
public void run(){
for (int i = 0; i < 50; i++) {
System.out.println(getName()+i);
}
}
}

public class ThreadDemo {
public static void main(String[] args) {
//创建子类对象(实际上就是创建一个线程对象)
MyThread mt = new MyThread();
//调用线程对象的start方法(启动该线程)
mt.start();
//主线程 不会等待子线程任务结束
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
}

}
总结:
a.可以给线程起名,也可以使用默认名
b.获取线程名称时,建议使用通用方式Thread.currentThread().getName();子线程内部,也可以直接调用getName();

1.5 创建新的线程方式二:实现方式
a.API描述:声明实现Runnable接口的类。该类然后实现run方法。然后可以分配该类的实例,在创建Thread时作为一个参数来传递并启动

b.分析创建步骤:
I.创建实现类 实现Runnable接口(实际上接口中就一个任务方法:run方法)
II.实现类重写run方法(run中编写具体的任务代码)
III.创建实现类对象(该实现类对象并不少线程对象,称为任务对象)
IV.创建Thread对象,同时传入实现类对象public Thread(Runnable r);
V.启动该线程(调用线程对象的start方法)

c.代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//创建实现类,实现Runnable接口
public class MyRunnable implements Runnable {
//实现类重写run方法(run中编写具体的任务代码)
@Override
public void run() {
//run方法中写任务代码
for (int i = 0; i < 50; i++) {
System.out.println(i);
}
}
}


public class TestThread {
public static void main(String[] args) {
//创建实现类对象(该实现类对象并不少线程对象,称为任务对象)
MyRunnable mr = new MyRunnable();
//创建Thread对象,同时传入实现类对象public Thread(Runnable r);
Thread tt = new Thread(mr);
//启动该线程(调用线程对象的start方法)
tt.start();
//主线程不会等待子线程执行完毕
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
}
}

1.5.1 两种线程的优劣势比较
两种创建线程的方式,实现方式比较好
I.实现方式的线程和任务是分开的,由程序员自己组合的,适合多个相同的程序代码的线程去共享同一个资源。
II.实现方式避免了java中的单继承的局限性。
III.实现方式线程和任务是接耦的,继承方式线程和任务是紧耦合的。增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
IV.线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的子类。
综上所述,在开发中建议使用实现方式(并不是说实现方式不对)

1.6 匿名内部类简化线程创建方式
匿名内部类的作用:可以快速创建一个类的子类对象,或者一个接口的实现类对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

public class TestDemo {
public static void main(String[] args) {
new Thread() {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
}
}.start();
new Thread (new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
}
}).start();

for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
}

}
二.高并发和线程安全
2.1 高并发及线程安全介绍
什么是高并发:在某个时间点上,大量的用户(线程),访问同一个资源。
线程安全:在某个时间点上,大量用户(线程)访问同一个资源时,由于线程运行机制的原因,可能导致被访问的资源出现类”数据污染”

2.2 多线程的运行机制[内存方面]

1
2
3
4
5
6
7
8
9
 public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
} }

public class Demo {
public static void main(String[] args) {
} }

2.3 多线程的安全性问题:可见性
当一个共享的变量被多个线程使用时,当其中某个线程对其进行了修改,该值对其他线程并非立即可见的,其他线程获取的值,还是以前的副本(旧值)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class MyThread extends Thread {
public static int a = 0;
@Override
public void run() {
System.out.println("线程启动,休息2秒...");
try {
Thread.sleep(1000 * 2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("将a的值改为1");
a = 1;
System.out.println("线程结束...");
}
}


public class TestSafeDemo01 {
public static void main(String[] args) {
//1.启动线程
MyThread t = new MyThread();
t.start();
//2.主线程继续
while (true) {
if (MyThread.a == 1) {
System.out.println("主线程读到了a = 1");
}
}
}
}

2.4 多线程的安全性问题:有序性
单线程会在不影响代码的结果的程度上,对代码进行重排。
如果在在”多线程”情况下,可能对一样的代码执行后得出不一样的结果。
我们要保证在多线程的情况下,不对代码进行”重排”,保证代码是有序的(不要使用重排)

2.5 多线程的安全性问题:原子性
什么是原子性:线程对一个共享变量进行++时,这个++是分成两部操作的,先取出值加1,然后给共享变量赋值,如果取出值加1后,还没有来得及赋值就被其他线程抢走CPU,此时我们称++操作不具有原子性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

public class MyThread extends Thread {
public static int a = 0;
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
a++;
}
System.out.println("Done");
}
}

public class TestSaftDemo {
public static void main(String[] args) throws InterruptedException {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start();//Thread 1对a加了一万次
t2.start();//Thread 2对a加了一万次

Thread.sleep(1000);
System.out.println(MyThread.a);
}
}

三.volatile关键字
3.1 volatile是什么
volatile是一个关键字,用来修饰成员变量(静态变量),被他修改的变量,具有可见性和有序性

3.2 volatile解决可见性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class MyThread extends Thread {
public volatile static int a = 0;
@Override
public void run() {
System.out.println("线程启动,休息2秒...");
try {
Thread.sleep(1000 * 2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("将a的值改为1");
a = 1;
System.out.println("线程结束...");
}
}

public class TestSafeDemo01 {
public static void main(String[] args) {
//1.启动线程
MyThread t = new MyThread();
t.start();
//2.主线程继续
while (true) {
if (MyThread.a == 1) {
System.out.println("主线程读到了a = 1");
}
}
}
}

3.3 volatile解决有序性

3.4 volatile不能解决原子性
即使加了volatile,仍然会被其他线程抢占,无法解决原子性问题

小结:volatile作用
a.解决变量的可见性,一旦变量发生改变,所有使用到该变量的线程都会取到最新值
b.解决变量的有序性,一旦变量加上volatile,那么编译器不会对变量运行的代码进行重排
c.无法解决变量操作过程中的原子性,对变量的操作哈斯可能被其他线程打断

四.原子类
4.1 原子类概述
a.什么是原子类:是对普通类型(如:int,Integer,double ,Double)的原子类封装,使其的操作成员原子操作
b.原子类的作用:对原子类的增强或减少操作,保证其原子性,保证中间不会被其他线程”打断”
c.原子类有哪些:

在java.util.concurrent.atomic包下定义了一些对“变量”操作的“原子类”:
1).java.util.concurrent.atomic.AtomicInteger:对int变量操作的“原子类”;
2).java.util.concurrent.atomic.AtomicLong:对long变量操作的“原子类”;
3).java.util.concurrent.atomic.AtomicBoolean:对boolean变量操作的“原子类”;
它们可以保证对“变量”操作的:原子性、有序性、可见性。

4.2 AtomicInteger类示例
a.AtomicInteger是什么:是对int类型变量进程操作原子类
b.AtomicInteger的构造方法:public AtomicInteger(int num);
c.AtomicInteger的成员方法:
public int getAndIncrement();//就相当于我们的 变量++
public int ncrementAndGet();//就相当于我们的 ++变量

d.AtomicInteger的改写案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MyThread extends Thread {
public static int a = 0;

@Override
public void run() {
for (int i = 0; i < 10000; i++) {
a++;
}
System.out.println("Done");
}
}


public class TestSaftDemo {
public static void main(String[] args) throws InterruptedException {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start();//Thread 1对a加了一万次
t2.start();//Thread 2对a加了一万次

Thread.sleep(1000);
System.out.println(MyThread.a);
}
}

4.3 AtomicInteger类底层的工作原理-CAS机制
CAS:Compare And Swap(自旋)
我们假设两个线程交替运行的情况,看看它是怎样工作的:
初始AtomicInteger的值为0
线程A执行:var5 = this.getIntVolatile(var1,var2);获取的结果为:0
线程A被暂停
线程B执行:var5 = this.getIntVolatile(var1,var2);获取的结果为:0
线程B执行:this.compareAndSwapInt(var1,var2,var5,var5 + var4)
线程B成功将AtomicInteger中的值改为1
线程A恢复运行,执行:this.compareAndSwapInt(var1,var2,var5,var5 + var4)
此时线程A使用var1和var2从AtomicInteger中获取的值为:1,而传入的var5为0,比较失败,返回 false,继续循环。
线程A执行:var5 = this.getIntVolatile(var1,var2);获取的结果为:1
线程A执行:this.compareAndSwapInt(var1,var2,var5,var5 + var4)
此时线程A使用var1和var2从AtomicInteger中获取的值为:1,而传入的var5为1,比较成功,将其修改 为var5 + var4,也就是2,将AtomicInteger中的值改为2,结束。

•CAS也被称为乐观锁:因为大部分比较的结果为true,就直接修改了。只有少部分多线程并发的情况会 导致CAS失败,而再次循环。

4.4 AtomicIntegerArray类示例
•非原子类数组在多线程并发时会有问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MyThread extends Thread {
public static int[] intArray = new int[1000];
@Override
public void run(){
for (int i = 0; i < intArray.length; i++) {
intArray[i]++;
}
}
}

public class TestDemo {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new MyThread().start();
}
Thread.sleep(1000*5);
System.out.println("sleep 5 second");
for (int i = 0; i <MyThread.intArray.length ; i++) {
System.out.println(MyThread.intArray[i]);
}
}
}

有极个别元素小于1000,因为int[]不是原子类数组,

•使用原子类数组,保证原子性,解决问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MyThread extends Thread {
public static int[] intArray = new int[1000];
public static AtomicIntegerArray arr = new AtomicIntegerArray(intArray);

@Override
public void run(){
for (int i = 0; i < arr.length(); i++) {
arr.addAndGet(i, 1);
}
}
}

public class TestDemo {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new MyThread().start();
}
Thread.sleep(1000*5);
System.out.println("sleep 5 second");
for (int i = 0; i <MyThread.arr.length() ; i++) {
System.out.println(MyThread.arr.get(i));
}
}
}

总结:
进程和线程的区别:
进程:正在内存中运行的程序
线程:进程中完成某个功能的执行单元

并发于并行的区别:
并行:两个线程真的一起运行
并发:两个线程看起来一起运行,实际交替执行

Java多线程原理[CPU]:
线程调度

继承子类方式创建多线程:
I.子类 继承 Thread
II.子类 重写 run
III.创建 子类 对象
IV.调用 子类 对象 start

接口实现类方式实现多线程:
I.实现类 实现 Runnable
II.实现类 重写 run
III.创建 实现类 对象
IV.创建 Thread对象 同时 传入 实现类 对象
V.调用 Thread 对象 start 方法
VI.使用匿名内部类快速创建继承方式和实现方式的线程

接口实现方式好处:
I.实现方式的线程和任务是分开的,由程序员自己组合的,适合多个相同的程序代码的线程去共享同一个资源。
II.实现方式避免了java中的单继承的局限性。
III.实现方式线程和任务是接耦的,继承方式线程和任务是紧耦合的。增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
IV.线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的子类。

安全问题出现的原因:
可见性 有序性 原子性

volatile关键字作用:
解决可见性与有序性,不能解决原子性

原子类AtomicInteger使用:
解决原子性
创建:
AtomicInteger i = new AtomicInteger(1);
使用:
i.getAndIncrement();//i++
i.incrementAndGet();//++i

原子类工作机制:
原子类底层使用CAS机制