0%

Java基础(多线程)

创建多线程的方式

1、继承Thread类

  1. 创建一个继承于Thread类
  2. 重写Thread类的 run( )方法
  3. 创建Thread类的子类对象
  4. 通过此对象调用 start( ):启动当前线程 —> 调用当前线程的run()
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
31
32
33
34
35
36
37
38
public class ThreadTest {
public static void main(String[] args) {
MyThread myThread = new MyThread();
//调用start
myThread.start();

//仍然在main线程中执行
for (int i = 0;i < 10;i++) {
if (i % 2 != 0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}

class MyThread extends Thread {
@Override
public void run() {
//声明线程需要执行的操作
for (int i = 0;i < 10;i++) {
if (i % 2 == 0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
/*结果:
main:1
main:3
main:5
main:7
main:9
Thread-0:0
Thread-0:2
Thread-0:4
Thread-0:6
Thread-0:8
*/

注意:

  • 不能通过直接调用run( )的方式启动线程
  • 再启动一个线程时,不可以让已经start()的线程执行。需要重新创建一个线程的对象。

为了减少代码量也可以 通过创建Thread类的匿名子类的方式

1
2
3
4
5
6
7
8
9
10
11
new Thread(){
@Override
public void run() {
//声明线程需要执行的操作
for (int i = 0;i < 10;i++) {
if (i % 2 == 0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}

2、实现Runnable接口

  1. 创建一个实现Runnable接口的类
  2. 实现类去实现Runnable中的抽象方法:run( )
  3. 创建实现类的对象
  4. 将此对象作为参数传递到Thread类的构造器中。创建Thread类的对象
  5. 通过Thread类的对象调用start( )方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ThreadTest {
public static void main(String[] args) {
MyThread2 myThread2 = new MyThread2();
Thread t1 = new Thread(myThread2);
t1.start();
}
}
class MyThread2 implements Runnable {

@Override
public void run() {
//声明线程需要执行的操作
for (int i = 0;i < 10;i++) {
if (i % 2 == 0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}

3、实现Callable接口

1.创建一个实现Callable的实现类

2.实现call方法,将此线程需要执行的操作声明在call()中

3.创建Callable接口实现类的对象

4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象

5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()

6.获取Callable中call方法的返回值

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
31
32
33
public class ThreadTest {
public static void main(String[] args) {
//创建Callable接口实现类的对象
NumThread numThread = new NumThread();
//将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
FutureTask<Integer> futureTask = new FutureTask<>(numThread);
//将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
new Thread(futureTask).start();
try {
//获取Callable中call方法的返回值
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
Object sum = futureTask.get();
System.out.println("总和为:" + sum);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
//创建一个实现Callable的实现类
class NumThread implements Callable<Integer> {
//实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 10; i++) {
if(i % 2 == 0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}

与使用Runnable相比, Callable功能更强大些

  1. 相比run()方法,可以有返回值
  2. 方法可以抛出异常
  3. 支持泛型的返回值
  4. 需要借助FutureTask类,比如获取返回结果

4、使用线程池

JDK 5.0起提供了线程池相关API:ExecutorService 和 Executors

  • ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
    • void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行 Runnable
    • Future submit(Callable task):执行任务,有返回值,一般又来执行 Callable
    • void shutdown() :关闭连接池
  • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
    • Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
    • Executors.newFixedThreadPool(n): 创建一个可重用固定线程数的线程池
    • Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
    • Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运 行命令或者定期地执行。
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
31
32
33
34
35
36
37
38
class NumberThread implements Runnable{

@Override
public void run() {
for(int i = 0;i <= 10;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
class NumberThread1 implements Runnable{

@Override
public void run() {
for(int i = 0;i <= 10;i++){
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//1. 提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;

//2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread());//适合适用于Runnable
service.execute(new NumberThread1());//适合适用于Runnable
// service.submit(Callable callable);//适合使用于Callable

//3.关闭连接池
service.shutdown();
}

}

Thread类的常用方法

  • start():启动当前线程;调用当前线程的run()
  • run(): 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
  • currentThread():静态方法,返回执行当前代码的线程
  • getName():获取当前线程的名字
  • setName():设置当前线程的名字
  • yield():释放当前cpu的执行权
  • join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。
  • stop():已过时。当执行此方法时,强制结束当前线程。
  • sleep(long millitime):让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态。
  • isAlive():判断当前线程是否存活

线程的优先级

  • MAX_PRIORITY:10
  • MIN _PRIORITY:1
  • NORM_PRIORITY:5 –>默认优先级

获取和设置当前线程的优先级

  • getPriority():获取线程的优先级
  • setPriority(int p):设置线程的优先级

说明:高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行,并不意味着只有当高优先级的线程执行完以后,低优先级的线程才会执行。

线程的生命周期

线程安全性

1、什么是线程安全性

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
也就是:多个线程访问同一变量或对象时,都能保证结果的正确性就是线程安全。

2、线程不安全产生的主要原因:

因为多个线程共享一个内存,所以当多个线程共享一个全局变量的时候,可能会受到其他干扰。如线程更新会先在本地内存更新,然后再同步到共享内存中,当多个线程同时读写的时候,数据会出现错误,就产生了线程不安全的现象。

3、 线程安全在三个方面体现

  • 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
  • 可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);
  • 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。
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
class MyThread2 implements Runnable {
private int count = 0;
@Override
public void run() {
while (true) {
if (count < 10) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(count+"\t");
count++;
} else {
break;
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
MyThread2 myThread2 = new MyThread2();
Thread t1 = new Thread(myThread2);
t1.start();
Thread t2 = new Thread(myThread2);
t2.start();
}
}

上述代码就会产生线程安全问题,因为两个线程共享了count变量。其运行结果为:

1
0	0	2	2	4	4	6	6	8	8	10

可见,结果是不正确的。

线程间的同步

在Java中,我们通过同步机制来解决线程的安全问题,有多种方式

方式一:同步代码块(synchronized)

1
2
3
synchronized(同步监视器){
//需要被同步的代码
}

说明:1、操作共享数据的代码,即为需要被同步的代码。 (不能包含代码多了,也不能包含代码少了。)

2、同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。

3、在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。

4、在继承Thread类创建多线程的方式中,可以考虑使用当前类(类.class)充当同步监视器。

要求:多个线程必须要共用同一把锁。

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
31
32
33
class MyThread2 implements Runnable {
private int count = 0;
Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
if (count < 10) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(count + "\t");
count++;
} else {
break;
}
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
MyThread2 myThread2 = new MyThread2();
Thread t1 = new Thread(myThread2);
t1.start();
Thread t2 = new Thread(myThread2);
t2.start();
}
}

//结果:0 1 2 3 4 5 6 7 8 9

不过,虽然它解决了线程的安全问题,但是操作同步代码时,实际上只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。

另外,我们也可以使用 同步方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
 public synchronized void test2(int j) {
if (count < 10) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(count + "\t");
count++;
} else {
break;
}
}

方式二:Lock锁( ReentrantLock )

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
31
32
33
34
35
36
37
38
39
40
41
class MyThread2 implements Runnable {
private int count = 0;

//实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();

@Override
public void run() {
while (true) {
try{
//调用锁定方法lock()
lock.lock();

if (count < 10) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(count + "\t");
count++;
} else {
break;
}
}finally {
//调用解锁方法:unlock
lock.unlock();
}

}
}
}
public class ThreadTest {
public static void main(String[] args) {
MyThread2 myThread2 = new MyThread2();
Thread t1 = new Thread(myThread2);
t1.start();
Thread t2 = new Thread(myThread2);
t2.start();
}
}

synchronized 与 Lock的异同?

相同:二者都可以解决线程安全问题。

不同:

  • synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器。
  • Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())。

volatile关键字*

一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,就具备了两层语义:

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
  • 禁止进行指令重排序
1
2
3
4
5
6
7
8
//线程1
boolean stop = false;
while(!stop){
doSomething();
}

//线程2
stop = true;

这段代码是存在线程安全问题的,它是有很大可能为变成一个死循环。因为当线程2更改了stop变量的值后,可能还没有来得及写入主存当中, 线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去 。

但是用volatile修饰之后就变得不一样了:

  1. 使用volatile关键字会强制将修改的值立即写入主存;
  2. 使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效;
  3. 由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。

那么在线程2修改stop值时,会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。

volatile关键字能保证可见性 ,但不能保证原子性,所以我们在前面的代码中将共享变量count用volatile修饰,也不能得出正确结果。

线程的通信

涉及到的3个方法:

  • wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。

    • notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
    • notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。

注意:1.wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。

2.wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。

sleep() 和 wait()的异同?

1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。

2.不同点:

1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait();

2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中;

3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。


-------------本文结束感谢您的阅读-------------