Java多线程编程(一)

最近在学习并发编程。准备学习路线是:《java多线程核心编程技术》(敲一遍)->《java并发编程实践》(理论掌握)->java.util.concurrent源码阅读。我将已经敲过的代码放入github仓库中,有兴趣的童鞋可以瞅瞅。
下面就记录一下我的一些心得体会~
本文介绍了多线程的基础,比如创建线程,以及synchronized和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
public class _1CreateNewThread {
public static void main(String[] args) {
class MyThread1 extends Thread {
@Override
public void run() {
super.run();
for (int i = 0; i < 10; i++)
System.out.println("MyThread1");
}
}
class MyThread2 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++)
System.out.println("MyThread2");
}
}
Thread thread1 = new MyThread1();
Thread thread2 = new Thread(new MyThread2());
thread1.start();
thread2.start();
}
}
线程启动顺序与调用start()方法顺序无关。

一个典型线程不安全例子

这个例子中,对变量a设置了延迟,导致必然出现线程不安全。加锁就可以恢复正常:

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
public class _3ThreadSafe {
public static void main(String[] args) {
ALogin a = new ALogin();
BLogin b = new BLogin();
a.start(); b.start();
}
static class LoginServlet {
private static String userNameRef;
private static String passWordRef;
public static void doPost (String userName, String passWord) {
try {
userNameRef = userName;
if (userName.equals("a"))
TimeUnit.SECONDS.sleep(1);
passWordRef = passWord;
System.out.println("username=" + userNameRef +
" password=" + passWordRef);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class ALogin extends Thread {
@Override
public void run() {
LoginServlet.doPost("a", "aa");
}
}
static class BLogin extends Thread {
@Override
public void run() {
LoginServlet.doPost("b", "bb");
}
}
}

此外,在main方法中,只能实现静态内部类或者非静态成员内部类。而静态类也有很多的坑,比如私有构造方法无效,可以new,等等。

syso与i–

syso方法是线程安全的,源码:

1
2
3
4
5
6
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}

但是,要小心其中的i–:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class _4SumNum {
public static void main(String[] args) {
MyThread run = new MyThread();
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
Thread t3 = new Thread(run);
Thread t4 = new Thread(run);
Thread t5 = new Thread(run);
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
static class MyThread extends Thread {
private int i = 5;
@Override
public void run() {
System.out.println(this.currentThread().getName() + " i=" + i--);
}
}
}

这个例子中,可以发现有一定的概率会出现线程安全问题。这是因为println()方法内在同步,但是i–的操作是进入方法前完成的,所以有几率发生问题。

几个常用方法

currentThread()方法

currentThread()方法返回代码段正在被哪个线程调用的信息,API的原文是

Returns a reference to the currently executing thread object.

若不重写Thread方法,此时this.currentThread()跟Thread.currentThread()无任何区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class _5CurrentThread {
public static void main(String[] args) {
Thread t1 = new CountOperate();
t1.setName("ThreadA");
t1.start();
}
static class CountOperate extends Thread {
public CountOperate() {
System.out.println("CountOperate--begin");
System.out.println("current thread:" + Thread.currentThread().getName());
System.out.println("this.getName():" + this.getName());
System.out.println("CountOperate--end");
}
@Override
public void run() {
System.out.println("run--begin");
System.out.println("current thread:" + Thread.currentThread().getName());
System.out.println("this.getName():" + this.getName());
System.out.println("run--end");
}
}
}

输出结果:

CountOperate--begin
current thread:main
this.getName():Thread-0
CountOperate--end
run--begin
current thread:ThreadA
this.getName():ThreadA
run--end

可以看到,Thread.currentThread()是main线程,this是当前线程。

isAlive()方法

isAlive()方法表示当前线程是否为活动状态,即线程已启动,且未终止的状态。API中的原文是:

Tests if this thread is alive. A thread is alive if it has been started and has not yet died.

sleep()方法

表示当前线程睡眠n个ms。需要检查异常InterrupttedException。
有一个很重要的知识点,同样是等待,sleep()和wait()的区别是什么呢?sleep()睡眠,但是并不释放锁。而wait()将锁释放,表示当前线程正在等待,只有notify()方法被调用后才会醒来。
此外,最好使用TimeUnit.··.sleep()方法来替代sleep()方法。因为sleep()只能表示ms,而TimeUnit类可以直观的表示睡眠了多少m,s,ms,ns,等,可读性很强。

getId()方法

获取线程唯一标识。并不可以通过setId()来设置其id。观察其源码,发现id是内部生成的,作为其标识。

终止线程

需要有技巧的安全的停止线程。主要有3个方法,后文将详细介绍。

  1. 使用退出标志,正常退出,即run方法完成后线程终止;
  2. stop()方法强制退出(这种方式已废弃);
  3. interrupt()方法中断线程。并不会强制终止,而是传入中断信号,还需要线程配合判断才能真正的终止。

interrupted()和isInterrupted()

两个方法的源码声明如下,可以发现非常的相像:

public boolean isInterrupted() {}
public static boolean interrupted() {}

两者的区别:

  • this.interrupted():测试当前是否中断,执行后将标志重置为false;
  • this.isInterrupted():测试线程Thread对象是否是中断状态,不清除标志位。

利用中断可以灵活的终止线程,if (this.interrupted())来检测

  • 仅仅跳出循环,可以检测时break;
  • 若需要退出线程,抛出InterrupttedException异常,并检测即可;
  • 也可以使用return来退出,但不如异常方式,因为可以一层一层向上抛出。

若处于睡眠,进行中断则会进入InterrupttedException异常中。需要注意的是,无论睡眠与否,若中断打开,那么都会抛出异常。

暂停线程suspend和resume方法

与stop一样都是被废除的方法。使用不当极易造成公共对象独占,导致死锁。而且还有不同步等缺点。

优先级

yield()方法

作用是放弃当前cpu资源,让给逼得资源。但放弃的时间不确定,有可能刚刚放弃,马上又获得了时间片。
在测试yield的方法时,发现电脑循环500w后用时2ms?然后循环5000w后还是2ms?实验室的电脑性能只能说一般,一定是哪里有问题。这个问题先搁置,以后有空解决下。

线程的优先级

不能依赖java中的优先级。优先级还是取决于系统,有些系统根本就不认可java的优先级。设置方法为:

1
thread.setPriority(n)

守护线程

java中有两种线程,一种是用户线程,一种是守护线程。
守护线程是一种特殊的线程,我理解为“保姆”线程。典型的守护线程就是垃圾回收线程。当进程中没有非守护线程,那么垃圾回收线程也就没有必要了,自动销毁。守护线程的存在意义就是被守护线程。
设置方法:

1
thread.setDaemon(true);


关键字synchronized 和 volatile

java中与同步相关的主要就是这两个关键字,下面来研究研究。

synchronized同步方法

syncronized意思是同步,相对应异步是asyncronized。syncronized为重量级锁,安全但是比较笨重。

私有变量线程安全

私有变量作用域仅在方法内部,因此不存在线程安全问题。而成员变量在方法外部,会产生竞争使用的问题,有可能线程不安全。

对象与锁

synchronized是对象锁,锁住的是对象,而不是代码。因此,对于不同的对象,每个对象有自己的锁。只有共享的资源才需要同步,若不是共享资源,那么根本没有同步的必要。
此外,在对象内部,若A先持有对象的锁,那么B可以异步调用对象的非synchronized方法;而B调用synchronized方法时则需要等待A释放锁。代码运行结果清晰地表示出来这个特性。

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
50
51
52
53
54
55
56
57
public class _2LockObject {
public static void main(String[] args) {
MyObject o = new MyObject();
Thread a = new MyThreadA(o);
a.setName("A");
Thread b = new MyThreadB(o);
b.setName("B");
a.start();
b.start();
}
static class MyObject {
synchronized public void methodA() {
try {
System.out.println("Begin method is " + Thread.currentThread().getName());
Thread.sleep(1000);
System.out.println("End time " + System.currentTimeMillis());
} catch (Exception e) {
e.printStackTrace();
}
}
//add lock
synchronized public void methodB() {
try {
System.out.println("Begin method is " + Thread.currentThread().getName()
+ " Begin time is " + System.currentTimeMillis());
Thread.sleep(1000);
System.out.println("End");
} catch (Exception e) {
e.printStackTrace();
}
}
}
static class MyThreadA extends Thread {
private MyObject object;
public MyThreadA(MyObject object) {
super();
this.object = object;
}
@Override
public void run() {
super.run();
object.methodA();
}
}
static class MyThreadB extends Thread {
private MyObject object;
public MyThreadB(MyObject object) {
super();
this.object = object;
}
@Override
public void run() {
super.run();
object.methodB();
}
}
}

脏读(dirty read)

脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还未同步,这时,另外一个事务也访问这个数据,然后使用了这个数据。下面这个例子中,getValue()先于setValue()执行完毕,此时setValue()执行了一半,数据未完全同步,因此会调用出错,出现脏读。

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
public class _3DirtyRead {
public static void main(String[] args) throws InterruptedException {
PublicVar p = new PublicVar();
Thread a = new MyThreadA(p);
a.start();
Thread.sleep(200);
p.getValue();
}
static class PublicVar {
public String username = "A";
public String password = "AA";
synchronized public void setValue(String username, String password) {
try {
this.username = username;
Thread.sleep(1000);
this.password = password;
System.out.println("setValue thread:" + Thread.currentThread().getName());
System.out.println("username:" + username + " password:" + password);
} catch (Exception e) {
e.printStackTrace();
}
}
// dirty read
// synchronized
public void getValue() {
System.out.println("getValue thread:" + Thread.currentThread().getName());
System.out.println("username:" + username + " password:" + password);
}
}
static class MyThreadA extends Thread {
private PublicVar publicVar;
public MyThreadA(PublicVar publicVar) {
super();
this.publicVar = publicVar;
}
@Override
public void run() {
super.run();
publicVar.setValue("B", "BB");
}
}
}

锁重入

锁重入指的是持有锁的线程试图获得锁时,请求会成功。换种说法,就是再一个synchronized方法内部调用本类其他synchronized方法时,永远会拿到锁。更为规范的说法是这样的:

可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后,内层递归函数仍然有获取该锁的代码,但不受影响。在JAVA环境下 ReentrantLock 和synchronized 都是可重入锁。

举个栗子:

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 _4LockIn {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
}
static class Service {
synchronized public void service1() {
System.out.println("service1");
service2();
}
synchronized public void service2() {
System.out.println("service2");
service3();
}
synchronized public void service3() {
System.out.println("service3");
// service1();
}
}
static class MyThread extends Thread {
@Override
public void run() {
super.run();
Service service = new Service();
service.service1();
}
}
}

这个例子中,service1()调用锁上的service2(),又调用service3()。印证了可以递归调用这一说法。
可重入锁最大的作用是避免死锁。以自旋锁作为例子:

1
2
3
4
5
6
7
8
9
10
11
12
public class SpinLock {
private AtomicReference<Thread> owner =new AtomicReference<>();
public void lock(){
Thread current = Thread.currentThread();
while(!owner.compareAndSet(null, current)){
}
}
public void unlock (){
Thread current = Thread.currentThread();
owner.compareAndSet(current, null);
}
}

对于自旋锁来说,

  1. 若有同一线程两调用lock() ,会导致第二次调用lock位置进行自旋,产生了死锁。说明这个锁并不是可重入的。(在lock函数内,应验证线程是否为已经获得锁的线程)
  2. 若1问题已经解决,当unlock()第一次调用时,就已经将锁释放了。实际上不应释放锁。(采用计数次进行统计)

设置标志位count,重入时+1,释放时-1。修改之后,如下:

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 SpinLock1 {
private AtomicReference<Thread> owner =new AtomicReference<>();
private int count =0;
public void lock(){
Thread current = Thread.currentThread();
if(current==owner.get()) {
count++;
return ;
}
while(!owner.compareAndSet(null, current)){
}
}
public void unlock (){
Thread current = Thread.currentThread();
if(current==owner.get()){
if(count!=0){
count--;
}else{
owner.compareAndSet(current, null);
}
}
}
}

该自旋锁即为可重入锁。
此外,可重入锁也支持父子类继承的环境中,子类可以通过可重入锁来调用父类的同步方法。

锁与异常

当一个线程代码出现异常时,锁会自动释放。

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
public class _5LockAndExeption {
public static void main(String[] args) {
Service s = new Service();
Thread a = new MyThreadA(s);
a.setName("a");
a.start();
Thread b = new MyThreadB(s);
b.setName("b");
b.start();
}
static class Service {
synchronized public void testMethod() {
if (Thread.currentThread().getName().equals("a")) {
System.out.println("Thread name is "+ Thread.currentThread().getName()
+ ". run beginTime is " + System.currentTimeMillis());
while (true) {
if (("" + Math.random()).substring(0, 8).equals("0.123456")) {
System.out.println("ThreadName is " + Thread.currentThread().getName()
+ ". run exceptionTime is " + System.currentTimeMillis());
Integer.parseInt("a");
}
}
} else
System.out.println("Thread B runTime is" + System.currentTimeMillis());
}
}
static class MyThreadA extends Thread {
private Service service;
public MyThreadA(Service service) {
this.service = service;
}
@Override
public void run() {
super.run();
service.testMethod();
}
}
static class MyThreadB extends Thread {
private Service service;
public MyThreadB(Service service) {
this.service = service;
}
@Override
public void run() {
super.run();
service.testMethod();
}
}
}

同步不能继承

当子类重写父类方法的时候,如果不加入同步标志,一样不具备同步性。

synchronized代码块

synchronized代码块更加灵活,只将需要同步的对象同步,而别的部分是异步执行。可以既保持同步又提高性能。
当一个线程访问synchronized(this)代码块,其他线程对同一个对象中其他synchronized(this)代码块访问将被阻塞。
推广来说,synchronized(x)中x可以为任意对象,有以下几个性质:

  1. 多个线程同时执行synchronized(x){}同步代码块时同步;
  2. 其他线程执行x对象内的synchronized同步方法同步;
  3. 其他线程执行x对象synchronized(this)代码块同步。

String的一些问题

String对象处于常量池中,同内容对象在内存中只有一份。如果持有String对象的锁,那么很容易造成不同对象持有相同的锁,造成线程同步问题。因此大多数情况下,同步synchronized代码块都不使用String作为锁对象,而改用其他。

volatile关键字

volatile主要有两个作用,分别是可见性与有序性。可见性指的是线程更新变量,所有线程都将更新这个变量。有序性指的是volatile变量代码段不使用重排序功能。
volatile实现可见性主要原理是这样的:每次读取该变量,强制从公共内存中读取,更改变量后,又写入公共内存中。但其最大的缺点就是不支持原子性。
volatile和synchronized都是同步关键字,有什么不同呢?主要是以下几点:

  1. volatile是线程同步的轻量级实现,性能好于synchronized。但随着JDK的发展,synchronized执行效率得到很大的提升。
  2. volatile只能修饰变量。synchronized可以修饰方法和代码块。
  3. 多线程访问volatile不会阻塞,而synchronized相反。
  4. volatile可以保证数据的可见性,但是不能保证原子性。synchronized可以保证原子性,因此也可以保证可见性。
  5. volatile解决的是变量在多个线程的可见性,而synchronized解决的是多个线程访问资源的同步性。

volatile关键字的 应用场景 :主要是在多个线程感知实例变量更改,可以获得最新的值使用。

原子类进行i++操作

i++貌似一句话,但实际上在虚拟机内部分解为几段代码,分别增加、赋值。因此,对于自增语句,volatile并不能保证线程安全。这种情况下,可以使用原子类来实现功能。
一个小例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.concurrent.atomic.AtomicInteger;
public class _12AtomicInteger {
public static void main(String[] args) {
Thread[] threadArr = new MyThread[10];
for (Thread t : threadArr) {
t = new MyThread();
t.start();
}
}
static class MyThread extends Thread {
private static AtomicInteger count = new AtomicInteger(0);
@Override
public void run() {
for (int i = 0; i < 1000; i++)
System.out.println(count.incrementAndGet());
}
}
}

原子类也不能保证完全正确

原子类(或者别的线程安全的类)只能保证自己的语句原子(或同步)执行,如果不加锁,语句和语句之间仍不同步。