0%

多线程创建及线程安全问题

线程与进程

进程:在计算中,进程是由一个或多个线程执行的计算机程序的实例。它包含程序代码及其活动。取决于操作系统(OS),一个进程可能由多个并行执行指令的执行线程组成。

线程:在计算机科学中,执行线程是可以由调度程序(通常是操作系统的一部分)独立管理的已编程指令的最小序列。

主要区别:进程是正在执行某些代码的程序,而线程是该进程中执行的独立路径。一个进程可以具有多个用于执行独立任务的线程,例如,一个用于从磁盘读取数据的线程,一个用于处理该数据的线程以及另一个用于通过网络发送该数据的线程。这种提高吞吐量并更好地利用CPU能力的技术也称为多线程。

Thread的常见方法

  1. start():启动当前线程:调用当前线程内的run()方法
  2. run():重写Thread()类中的run()方法,方法体为线程需要执行的操作
  3. currentThread():静态方法,返回执行当前代码的线程
  4. getName():获取当前线程的名称
  5. setName():设置当前线程的名称
  6. yield():静态方法。使当前正在执行的线程向另外一个线程交出运行全
  7. join():在线程a中调用线程b的join(),此时线程a为阻塞状态,直到线程b完全执行完以后,线程a结束阻塞状态
  8. sleep():让当前线程睡眠指定的毫秒数(millitime),在指定毫秒内,当前线程是阻塞状态
  9. isAlive():判断线程是否存活

线程的生命周期

多线程的创建

继承Thread类

  1. 创建一个Thread类的子类
  2. 重写run()方法 —> 将线程需要执行的操作写入run()方法体
  3. 创建Thread类的对象
  4. 通过此对象调用start()方法

代码实施

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
42
43
44
45
// 1. 创建继承于Thread类的子类
class MyThread1 extends Thread{
// 2. 重写run()方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i%2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}


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

// 给主线程取名
Thread.currentThread().setName("主线程");


// 3. 创建Thread类的对象
MyThread1 m1 = new MyThread1();

// 给线程1取名
m1.setName("线程1");

// 4. 调用start()方法启动线程
m1.start();



// 匿名子类的方式
new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i%2 != 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}.start();
}
}

实现Runnable类

  1. 创建一个实现了Runnable接口的类
  2. 实现类去实现Runnable接口中的抽象方法:run()
  3. 将此对象作为参数传递到Thread类的构造器中,并创建对象
  4. 通过Thread类的对象调用start()方法

代码实施

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 1. 创建一个实现了Runnable接口的类
class MThread implements Runnable{
// 2. 实现类去实现Runnable中的抽象方法:run()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i%2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i);
}
}
}
}


public class ThreadTest1 {
public static void main(String[] args) {
// 3. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
MThread mThread = new MThread();
// 4. 通过Thread类的对象调用start() 启动线程 调用当前线程的run()
Thread t1 = new Thread(mThread);
}
}

实现Callable类

该方式创建多线程比Runnable方法更强大:

  1. call()方法可以有返回值
  2. call()方法可以抛出异常
  1. 创建一个实现Callable的实现类
  2. 实现call方法,并将线程需要执行的操作声明在方法体内
  3. 创建Callable接口实现类的对象
  4. 将Callable接口实现类的对象作为参数传递到FutureTask构造器中,并创建FutureTask的对象
  5. 将FutureTask的对象传递到Thread类的构造器中,创建Threa类的对象,并调用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
34
35
36
37
38
39
40
41
// 1. 创建一个实现Callable的实现类
class NumThread implements Callable {
// 2. 实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
if(i%2 == 0){
System.out.println(i);
sum += i;
}
}
return sum; // 基本数据类型自动转包装类 --> 自动装箱
}
}


public class ThreadNew {
public static void main(String[] args) {
// 3. 创建callable接口实现类的对象
NumThread numThread = new NumThread();

// 4. 将此callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(numThread);

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

try {
// 6. 获取Callable中call方法的返回值
// get() 返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
Object sum = futureTask.get();
System.out.println("总和为:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}

}
}

使用线程池

好处:

  1. 提高响应速度(减少创建新线程的时间)
  2. 降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
  3. 便于线程管理
  1. 提供指定线程数量的线程池
  2. 执行指定线程的操作。提供Runnable或者Callable接口实现类的对象
  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
31
32
33
34
35
36
37
38
39
40
41
42
43
// 采用实现Runnable接口实现类的多线程创建
class NumberThread implements Runnable{
@Override
public void run() {
for (int i = 0; i <= 100; 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);
// ExecutorService 为接口


// 查询实现ExecutorService接口的实现类所在类
// System.out.printlln(service.getClass()) --> 输出ThreadPoolExecutor


// 创建实现ExecutorService接口实现类的对象
ThreadPoolExecutor service1 = (ThreadPoolExecutor)service;

// 设置线程池的属性(对象.属性)
// System.out.println(service.getClass()); // 查询对象所在的类

/* service1.setCorePoolSize();
service1.setKeepAliveTime();
service1.setMaximumPoolSize();*/


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

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

java 创建线程的三种方式、创建线程池的四种方式

线程安全与同步

当多个线程访问某个类时,该类始终都表现正确行为,则称该类是线程安全的。

在多线程访问环境中,对于共享数据的访问引起数据的不一致性,则此时是线程不安全的。需要考虑线程同步,来处理并发访问带来的问题。

synchronized

同步代码块

1
2
3
synchronized(同步监视器){
// 需要被同步的代码
}
  1. 需要同步的代码:操作共享数据的代码,不可以包含多,也不可以包含少。
  2. 共享数据:多个线程共同操作的变量。
  3. 同步监视器:俗称:锁。任何一个类的对象都可以充当锁。但是要求多个线程必须共用一把锁。

实现Runnable接口

由于采用实现Runnable接口的方式,只需要创建一个实现Runnable接口的实现类的对象即可,所以在同步时,同步监视器填写的最简单方式为this。

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
42
43
44
45
46
47
48
49
// 例子:创建三个窗口卖票,总票数为100张,使用实现Runnable接口的方式
class Window1 implements Runnable {

private int ticket = 100;

@Override
public void run() {
while(true){
synchronized(this){ // 同步监视器:this-->Window1的对象 --> 下方的w
if(ticket > 0){
try {
// 睡眠100ms
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(Thread.currentThread().getName() + ": 卖票,票号为:" + ticket);
ticket--;
}else{
break;
}
}
}
}
}


public class WindowsTest1 {
public static void main(String[] args) {
Window1 w = new Window1();

// 创建三个线程
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);

t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");

t1.start();
t2.start();
t3.start();


}

}

继承Thread类

由于采用继承Thread的方式,所以n个线程就需要建n个对象实现多线程。调用继承Thread继承类时,会重复造对象。所以里面的共有数据必须是静态的。在进行synchronized同步代码块时,同步监视器必须是静态对象,不能是this!除此之外也可以是继承类.class返回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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class WindowTest2 {
public static void main(String[] args) {
Window2 t1 = new Window2();
Window2 t2 = new Window2();
Window2 t3 = new Window2();

t1.setName("window-1");
t2.setName("window-2");
t3.setName("window-3");

t1.start();
t2.start();
t3.start();
}


}

class Window2 extends Thread{

private static int ticket = 100;
private static Object obj = new Object();

@Override
public void run() {
while(true){
// 正确的
synchronized (Window2.class){
// synchronized(obj){
// 错误的
// synchronized(this){
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(getName() + ": 卖票,票号为: " + ticket);
ticket--;
}else{
break;
}
}
}
}
}

同步方法

同样使用synchronized关键字,在同步方法中,用来修饰方法。

然后在run()方法中调用该方法。

1
2
3
private synchronized void show(){
// 方法体
}

继承Thread类

此时对应的同步监视器为对应继承类的Class类对象

实现Runnable接口

此时对应的同步监视器为this(实现Runnable接口的实现类的对象)

Lock

和synchronized不同的是,Lock需要手动的进行加锁和解锁。

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
42
43
44
45
class Window implements Runnable{
private int ticket = 100;
// 1. 实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock(true);

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

if(ticket > 0){
System.out.println(Thread.currentThread().getName() + ": 售票,票号为:" + ticket);
ticket--;
}else {
break;
}
} finally {
// 3. 调用解锁方法;unlock()
lock.unlock();
}
}
}
}


public class LockTest {
public static void main(String[] args) {
Window w = new Window();

Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);


t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");

t1.start();
t2.start();
t3.start();
}
}

单例设计模式之懒汉式(线程安全)

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
// 使用同步机制将单例模式中的懒汉式改写成线程安全的

public class BankTest {

}

class Bank{
private static Bank instance = null;
public static Bank getInstance(){
// 方式一:效率稍差
/* synchronized (Bank.class) {
if(instance == null){
instance = new Bank();
}
return instance;
}*/
//方式二:效率稍高
if(instance == null){
synchronized (Bank.class) {
if(instance == null){
instance = new Bank();
}
}
}
return instance;

}
}

这种写法能够在多线程中很好的工作,但是每次调用getInstance()方法时都需要进行同步,造成不必要的同步开销。

参考: 单例模式(饿汉模式、懒汉模式(线程安全和线程不安全))