多线程基础(一)

线程安全

  • 概念:
    当多个对象访问某一个类(对象或方法)时,这个类始终都表现出正确的行为,那么这个类(对象或方法)就是线程安全的。
  • 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
public class MyThread extends Thread {

private int count = 5;

//synchronized
public synchronized void run(){
count--;
System.out.println(Thread.currentThread().getName()+" count="+count);
}

public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread thread1 = new Thread(myThread,"t1");
Thread thread2 = new Thread(myThread,"t2");
Thread thread3 = new Thread(myThread,"t3");
Thread thread4 = new Thread(myThread,"t4");
Thread thread5 = new Thread(myThread,"t5");

thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
}
}
  • 总结:
    当多个线程访问MyThread的run方法时,以排队的方式进行处理(按照CPU分配的先后顺序而定)。当一个线程执行synchronized修饰的方法里的代码时,首先尝试获得锁,如果拿到锁,执行方法体中的内容,拿不到锁时,就会不断尝试获取这把锁,直到拿到为止。这里存在多个线程的锁竞争问题。

多个线程多个锁

  • 概念:
    多个线程,每个线程都可以拿到自己指定的锁,分别获得锁之后,执行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
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
58
59
60
public class MultiThread{

//加static
private static int num = 0;

//加static
public static synchronized void print(String tag){
try{
if(tag.equals("a")){
num = 100;
System.out.println("tag a set num over!");
Thread.sleep(3000);
}else{
num = 200;
System.out.println("tag b set num over!");
}
System.out.println("tag" + " = " + tag + ",and num= " + num);
} catch(InterruptedException e){
e.printStackTrace();
}
}

public static void main(String[] args) {
final MultiThread m1 = new MultiThread();
final MultiThread m2 = new MultiThread();

Thread t1 = new Thread(new Runnable(){
@Override
public void run() {
//获得m1对象锁
m1.print("a");
}
},"t1");

Thread t2 = new Thread(new Runnable(){
@Override
public void run() {
//获得m2对象锁
m2.print("b");
}
},"t2");

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

}
/**
* 加static之前(对象锁):
* tag a set num over!
* tag b set num over!
* tag = b,and num= 200
* tag = a,and num= 100
*
* 加static之后(类锁):
* tag a set num over!
* tag = a,and num= 100
* tag b set num over!
* tag = b,and num= 200
* */
  • 总结:
    关键字synchronized获取到的都是对象锁,所以示例代码中两个线程在执行synchronized修饰的方法时,获得的是两个不同的对象锁,他们互不影响。当在静态方法上加synchronized时,表示锁定.class类,即类级别的锁(独占.class类)。

对象锁的同步和异步

  • 同步:synchronized
    同步的概念就是共享,这里“共享”是关键点,如果不是共享的资源,就没必要同步。
  • 异步:asynchronized
    异步的概念就是独立,相互之间不受到任何制约。就好比我们学习http时,当页面发起的Ajax请求时,我们还可以浏览网页,二者之间没有任何关系。

同步的目的就是为了线程安全,对于线程安全来说,需要满足两个特性:

  • 原子性
  • 可见性
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
public class MyObject {

//synchronized
public synchronized void method1() {
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

//asynchronized
public void method2(){
System.out.println(Thread.currentThread().getName());
}

public static void main(String[] args) {
MyObject obj = new MyObject();

Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
obj.method1();
}
},"t1");

Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
obj.method2();
}
},"t2");

t1.start();
t2.start();
}
}
/**
* 立即打印:
* t1
* t2
* */
  • 总结:
    A线程持有对象锁时,B线程如果这个时候调用对象中的同步(synchronized)方法则需要等待,也就是同步。
    A线程持有对象锁时,B线程如果这个时候调用对象中的非同步(非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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class DirtyRead {

String username = "zhangsan";
String password = "123";

public synchronized void setValue(String username,String password){
this.username = username;

try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.password = password;
System.out.println("setValue后的username= "+username+", password= "+password);
}

//synchronized
public void getValue(){
System.out.println("getValue的username= "+username+", password= "+password);
}

public static void main(String[] args) throws InterruptedException {
final DirtyRead dr = new DirtyRead();

Thread t1 = new Thread(new Runnable(){
@Override
public void run() {
dr.setValue("zhangsan", "456");
}
});
t1.start();

Thread.sleep(1000);

dr.getValue();
}
}
/**
*打印结果:
* getValue的username= zhangsan, password= 123
* setValue后的username= zhangsan, password= 456
*
*想要的结果:
*setValue后的username= zhangsan, password= 456
*setValue后的username= zhangsan, password= 456
* */
  • 总结:在我们对一个对象的方法加锁的时候,需要考虑业务的整体性,即为setValue/getValue方法同时加锁,保证业务的原子性,不然就会出现业务错误。

    synchronized其他概念

  • synchronized锁重入:
    synchronized拥有锁重入的功能,当使用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
29
30
31
32
33
public class SyncDubbo {

public synchronized void method1(){
System.out.println("method1...");
//锁重入
method2();
}
public synchronized void method2(){
System.out.println("method2...");
//锁重入
method3();
}
public synchronized void method3(){
System.out.println("method3...");
}

public static void main(String[] args) {
final SyncDubbo sd = new SyncDubbo();

Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
sd.method1();
}
});
t1.start();
}
}
/**打印结果:
* method1...
* method2...
* method3...
* */
  • 出现异常时,锁自动释放:
    对于web应用程序,异常释放锁的情况,如果不及时处理,很可能对你的应用程序业务逻辑产生严重错误,比如你现在在执行一个队列任务,很多对象都去等待一个对象正确执行完毕并释放锁,但是第一个对象由于异常的出现,导致业务逻辑没有正确执行,就释放了锁,那么可想而知执行的都是错误的逻辑。所以这一点要引起注意,在编码时要考虑周全。

    synchronized代码块

    使用synchronized声明的方法在某些情况下是有弊端的,比如A线程调用同步方法执行一个很长时间的任务,那么B线程就必须等待比较长的时间才能执行,这样的情况就可以使用synchronized代码块去优化代码执行时间,也就是缩小锁的粒度。
  • synchronized可以使用任意的Object进行加锁。
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
58
59
60
61
62
63
64
65
public class ObjectLock {

public void method1(){
synchronized(this){//对象锁
try {
System.out.println(Thread.currentThread().getName()+" method1...");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public void method2(){
synchronized (ObjectLock.class) {
try {
System.out.println(Thread.currentThread().getName()+" method2...");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

private Object lock = new Object();
public void method3(){
synchronized (lock) { //任何对象锁
try {
System.out.println(Thread.currentThread().getName()+" method3...");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final ObjectLock objectLock = new ObjectLock();

Thread t1 = new Thread(new Runnable(){
@Override
public void run() {
objectLock.method1();
}
},"t1");

Thread t2 = new Thread(new Runnable(){
@Override
public void run() {
objectLock.method2();
}
},"t2");

Thread t3 = new Thread(new Runnable(){
@Override
public void run() {
objectLock.method3();
}
},"t3");

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

}
  • 不要使用String的常量加锁,会出现死循环。
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
58
59
60
public class StringLock {

public void method(){
//new String("字符串常量")
synchronized ("字符串常量") {
try {
while(true){
System.out.println(Thread.currentThread().getName()+"线程开始!");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"线程结束!");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) {
final StringLock sl = new StringLock();

Thread t1 = new Thread(new Runnable(){
@Override
public void run() {
sl.method();
}},"t1");

Thread t2 = new Thread(new Runnable(){
@Override
public void run() {
sl.method();
}},"t2");

t1.start();
t2.start();
}
}
/**
* 使用字符串常量时(一直给一个常量引用加锁):出现死循环
* t2线程开始!
* t2线程结束!
* t2线程开始!
* t2线程结束!
* t2线程开始!
* t2线程结束!
* t2线程开始!
* t2线程结束!
* t2线程开始!
*
* 使用new String()时:
* t1线程开始!
* t2线程开始!
* t1线程结束!
* t1线程开始!
* t2线程结束!
* t2线程开始!
* t1线程结束!
* t2线程结束!
* t2线程开始!
* t1线程开始!
*/
  • 锁对象改变问题:
    当一个对象加锁的时候,要注意对象本身发生改变的时候,持有的锁就不同。如果对象本身不发生改变,那么依然是同步的,即使对象的属性发生改变。
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
public class ChangeLock {

private String lock = "lock";

public void method(){
synchronized (lock) {
try{
System.out.println(Thread.currentThread().getName()+" start");
lock="change lock";//修改lock,对象本身发生改变,第二个线程持有不同对象锁
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+" end");
}catch(InterruptedException e){
e.printStackTrace();
}
}
}

public static void main(String[] args) {
final ChangeLock cl = new ChangeLock();

Thread t1 = new Thread(new Runnable(){
@Override
public void run() {
cl.method();
}},"t1");

Thread t2 = new Thread(new Runnable(){
@Override
public void run() {
cl.method();
}},"t2");

t1.start();
t2.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
46
public class ModifyLock {

private String username = "zhangsan";

private String password = "123";

public synchronized void modify(String username,String password){

try {
this.username = username; //修改对象属性值,持有锁不变
System.out.println(Thread.currentThread().getName()+" start");
Thread.sleep(1000);
this.password = password; //修改对象属性值,持有锁不变
System.out.println(Thread.currentThread().getName()+" end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public static void main(String[] args) {

final ModifyLock modifyLock = new ModifyLock();

Thread t1 = new Thread(new Runnable(){
@Override
public void run() {
modifyLock.modify("lisi", "456");
}},"t1");

Thread t2 = new Thread(new Runnable(){
@Override
public void run() {
modifyLock.modify("wangwu", "789");
}},"t2");

t1.start();
t2.start();
}
}
/**
* 打印结果:
* t1 start
* t1 end
* t2 start
* t2 end
* */

volatile关键字

  • 概念:
    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
30
31
32
33
34
35
36
37
38
39
40
public class RunThread extends Thread{

//volatile
private volatile boolean isRunning = true;

public void setRunning(boolean isRunning){
this.isRunning = isRunning;
}

public void run(){
System.out.println("进入run方法...");
while(isRunning == true){
//
}
System.out.println("线程停止...");
}

public static void main(String[] args) throws InterruptedException {
RunThread rt = new RunThread();

rt.start();
Thread.sleep(3000);
rt.setRunning(false);
System.out.println("isRunning的值被设置了false");
Thread.sleep(1000);
System.out.println(rt.isRunning);
}

}
/**
* isRunning无volatile修饰时:
* 进入run方法...
* isRunning的值被设置了false
* false
*isRunning被volatile修饰时:
* 进入run方法...
* isRunning的值被设置了false
* 线程停止...
* false
* */
  • 总结:
    在java中,每一个线程都有一块工作内存,其中存放着所有线程共享的主内存中的变量值得拷贝。当线程执行时,他在自己的工作内存中操作这些变量。为了存取一个共享变量,一个线程通常获取锁定并清除他的内存工作区,把这些共享变量从所有线程共享的内存区中正确的装入到他自己的工作内存区中,当线程解锁时保证该工作区中的变量的值写回到共享内存中。

一个线程可以执行的操作有使用(use)、赋值(assign)、装载(load)、存储(store)、锁定(lock)、解锁(unlock)。

而主内存可以执行的操作有读(read)、写(write)、锁定(lock)、解锁(unlock)。每个操作都是原子的。

volatile的作用就是强制线程到主内存(共享内存)里去读取变量,而不是去线程工作内存区里去读取,从而实现多个线程间的变量可见,也就是满足线程安全的可见性。

volatile关键字的非原子性

  • 概念:
    volatile关键字虽然拥有多个线程之间的可见性,但是却不具备同步性(也就是原子性),可以算上一个轻量级的synchronized,性能比synchronized强。但是需要注意:一般volatile用于只针对多个线程可见变量的操作,并不能替代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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class VolatileNotAtom extends Thread {

//volatile
//private static volatile int count;

private static AtomicInteger count = new AtomicInteger(0);

private static void addCount(){
for(int i = 0; i < 1000; i++){
//count++;
count.incrementAndGet();
}
System.out.println(count);
}

public void run(){
addCount();
}

public static void main(String[] args) {
VolatileNotAtom[] vna = new VolatileNotAtom[10];

for(int i = 0; i < 10; i++){
vna[i] = new VolatileNotAtom();
}

for(int i = 0; i < 10; i++){
vna[i].start();
}
}
}
/**
* volatile不能实现原子性:
* ...
* 7679
* 8679
* 3679
* 9679
*
* 使用AtomicInteger后:
* ...
* 6422
* 7000
* 8000
* 9000
* 10000
* */
  • 总结:
    volatile关键字只具有可见性,没有原子性。要实现原子性建议使用atomic类的系列对象,支持原子性操作(注意atomic类只保证本省方法原子性,并不保证多次操作的原子性)。
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
public class AtomicUse {

private static AtomicInteger count = new AtomicInteger(0);

//synchronized
public int multiAdd(){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
count.addAndGet(1);
count.addAndGet(2);
count.addAndGet(3);
count.addAndGet(4); //作整十操作

return count.get();
}

public static void main(String[] args) {
final AtomicUse au = new AtomicUse();

List<Thread> ts = new ArrayList<>();
for(int i = 0; i < 100; i++){
ts.add(new Thread(new Runnable(){
@Override
public void run() {
System.out.println(au.multiAdd());
}
}));
}
for(Thread t : ts){
t.start();
}
}
}

/**
* 增加synchronized修饰时打印结果:
* ...
* 960
* 970
* 980
* 990
* 1000
*
* 没有synchronized修饰时打印结果(出现了不是整十的数字13):
* ...
* 13
* 30
* 20
* 40
* 110
* */
-------------本文结束感谢您的阅读-------------