【学习笔记】多线程学习

一、概述

  • 程序:指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。

  • 进程:是执行程序的一次执行过程,是由系统资源分配的单位。

  • 线程:一个进程之中可以包含多个线程,并且至少有一个线程。线程是CPU调度和执行的单位。

  • main:用户线程;gc:守护线程。

  • 线程是独立执行的路径。

  • 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程。

  • main()称为主线程,为系统的入口,用于执行整个程序。

  • 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干扰的。

  • 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制。

  • 线程会带来额外的开销,如cpu调度时间,并发控制开销。

  • 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。

二、线程创建

  1. 继承Thread类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    //在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干扰的。
    // 1. 继承Thread类
    // 2. 重写run方法
    // 3. 调用start()方法
    public class MyThread extends Thread{
    @Override
    public void run() {
    // run方法线程
    for (int i = 0; i < 2000; i++) {
    System.out.println("我在看代码"+i);
    }
    }
    public static void main(String[] args) {
    MyThread myThread = new MyThread();
    // 调用线程
    myThread.start();
    for (int i = 0; i < 2000; i++) {
    System.out.println("我在学习多线程"+i);
    }
    }
    }
  2. 实现Runnable接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 1. 实现Runnable接口
    // 2. 重写run方法
    // 3. 调用start()方法
    public class MyRunnable implements Runnable {
    @Override
    public void run() {
    for (int i = 0; i < 200; i++) {
    System.out.println("子线程运行中!");
    }
    }

    public static void main(String[] args) {
    Runnable runnable = new MyRunnable();
    Thread thread = new Thread(runnable);
    thread.start();
    for (int i = 0; i < 200; i++) {
    System.out.println("主线程进行中");
    }
    }
    }
  3. 实现Callable接口

    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 MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
    for (int i = 0; i < 200; i++) {
    System.out.println(Thread.currentThread().getName() + "线程运行中!");
    }
    return "子线程执行完毕";
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
    // 创建执行服务
    ExecutorService service = Executors.newFixedThreadPool(3);
    // 提交执行
    Future<String> r1 = service.submit(new MyCallable());
    Future<String> r2 = service.submit(new MyCallable());
    Future<String> r3 = service.submit(new MyCallable());
    //获取执行结果
    String s1 = r1.get();
    String s2 = r2.get();
    String s3 = r3.get();
    //关闭服务
    service.shutdown();
    }
    }

三、线程状态与操作

  • setPriority():更改线程优先级
  • sleep():休眠
  • join():等待该线程终止
  • yield():暂停当前正在执行的线程对象,并执行其他线程
  • interrupt():中断线程,不推荐使用
  • isAlive():测试线程是否处于活动状态

1.停止线程

  • 不推荐使用JDK提供的stop、destory等过时的方法
  • 建议线程正常停止。例如可以使用一个标志位终止变量,设置标志位为false,则终止线程。
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
public class TestStop implements Runnable {
private boolean flag = true;

@Override
public void run() {
int i = 0;
while (flag) {
System.out.println("run..Thread" + i++);
}
}

public void stop() {
this.flag = false;
}

public static void main(String[] args) {
TestStop testStop = new TestStop();
new Thread(testStop).start();
for (int i = 0; i < 1000; i++) {
System.out.println("mian"+i);
if (i == 900) {
testStop.stop();
System.out.println("线程该停止了");
}
}
}
}

2.线程休眠 sleep

  • sleep(时间)指定当前线程阻塞的毫秒数。例如sleep(1000)表示停止1 s。
  • sleep存在异常InterruptedException
  • sleep时间达到后线程就进入就绪状态。
  • sleep可以模拟网络延迟,倒计时等。
  • 每一个对象都有一个锁,sleep不会释放锁。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class TestSleep implements Runnable {
private int i = 10;
@Override
public void run() {
while (i > 0) {
try {
Thread.sleep(1000);
System.out.println(i--);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) {
TestSleep testSleep = new TestSleep();
new Thread(testSleep).start();
System.out.println("---");
}
}

3.线程礼让yield

  • 让当前正在执行的线程暂停,但不阻塞。
  • 将线程从运行状态转换为就绪状态。
  • 让cpu重新调度,但礼让不一定成功,礼让后重新进入cpu调度状态,同时竞争状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestYield implements Runnable{
public static void main(String[] args) {
TestYield yield = new TestYield();
new Thread(yield,"a").start();
new Thread(yield,"b").start();
}
@Override
public void run() {
System.out.println(Thread.currentThread() + "线程开始执行");
Thread.yield();
System.out.println(Thread.currentThread() + "线程停止执行");
}
}

4.插队Join

  • Join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞
  • 可以理解为插队。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TestJoin implements Runnable {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("vip来了"+i);
}
}

public static void main(String[] args) throws InterruptedException {
// 启动线程
TestJoin testJoin = new TestJoin();
Thread thread = new Thread(testJoin);
thread.start();
// 主线程
for (int i = 0; i < 100; i++) {
if (i == 20) {
thread.join();
}
System.out.println("main" + i);
}
}
}

5.线程状态

  • NEW:线程尚未启动时候处于此状态。
  • RUNNABLE:在Java虚拟机中执行的线程处于此状态。
  • BLOCKED:被阻塞等待监视器锁定的线程处于此状态。
  • WAITING:正在等待另一个线程执行特定动作的线程处于此状态。
  • TIMED_WAITING:正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。
  • TERMINATED:已退出的线程处于此状态。
  • 一个线程可以在给定时间点处于一个状态,这些状态是不反映任何操作系统线程状态的虚拟机状态。
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 TestState {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("////////");
});
// 观察状态 new
Thread.State state = thread.getState();
System.out.println(state);

// 观察状态 run
thread.start();
state = thread.getState();
System.out.println(state);

// 观察状态 TIMED_WAITING(sleep时) 与 TERMINATED
while (state!= Thread.State.TERMINATED){
Thread.sleep(100);
state = thread.getState();
System.out.println(state);
}
}
}

6.线程的优先级

  • Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。
  • 线程优先级用数字表示,范围从1到10。
    • 最小值:Thread.MIN_PRIORITY=1
    • 最大值:Thread.MAX_PRIORITY=10
    • 默认值:Thread.NORM_PRIORITY=5
  • 使用以下方式可以改变或获取优先级:getPriority()setPriority(int xxx)
  • 优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用了。这都取决于cpu的调度。
  • 优先级需要在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
public class TestPriority  implements Runnable{
public static void main(String[] args) {
// 主线程默认优先级
System.out.println(Thread.currentThread().getName()+"-->"
+Thread.currentThread().getPriority());
TestPriority priority = new TestPriority();
Thread t1 = new Thread(priority);
Thread t2 = new Thread(priority);
Thread t3 = new Thread(priority);
Thread t4 = new Thread(priority);
// 先设置优先级,再启动
t1.start();
t2.setPriority(1);
t2.start();
t3.setPriority(4);
t3.start();
t4.setPriority(Thread.MAX_PRIORITY);
t4.start();
}

@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"-->"
+Thread.currentThread().getPriority());
}
}

7.守护(daemon)线程

  • 线程分为 用户线程 和 守护线程。
  • 虚拟机必须确保用户线程执行完毕。
  • 虚拟机不用等待守护线程执行完毕。
  • 守护线程包括:后台记录操作日志,监控内存,垃圾回收等。
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 TestDaemon {
public static void main(String[] args) {
God god = new God();
User user = new User();
new Thread(user).start();
Thread threadGod = new Thread(god);
// 默认是false表示用户线程。
threadGod.setDaemon(true);
threadGod.start();
}
}

class User implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("------开心------");
}
System.out.println("------over------");
}
}

class God implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("------守护------");
}
}
}

四、线程同步

  • 并发:一个对象同时被多个线程同时操作。
  • 处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。这时候就需要线程同步。
  • 在现实生活中,当我们遇到“同一资源,多个人都想使用”的情况时,最简单的办法就是排队。而线程同步其实就是一个等待(排队)机制,多个需要同时访问此对象的线程进入这个 对象的等待池 形成队列,等待前面线程使用完毕,下一个线程再使用。

1.同步=队列+锁

  • 由于同一进程的多个线程共享同一块储存空间,在带来方便的同时,也带来了访问的冲突问题,为了保证数据在访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可。但会存在以下问题:
    • 一个线程持有锁会导致其他所有需要此锁的线程挂起,
    • 在多线程竞争下,加锁,释放锁会导致比较多的 上下文切换调度延迟,引起性能问题。
    • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题。
  • 线程不安全的原因:每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。

2.同步方法

  • 由于我们可以通过private 关键字保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,它包含两种用法:synchronized方法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
public class SafeBuyTicket {
public static void main(String[] args) {
BuyTicket station = new BuyTicket();
new Thread(station,"user1").start();
new Thread(station,"user2").start();
new Thread(station,"user3").start();
}
}
class BuyTicket implements Runnable {
int ticketNums = 10;// 票
boolean flag = true;
@SneakyThrows
@Override
public void run() {
while (flag) {
buy();// 买票
}
}
private synchronized void buy() throws InterruptedException {
if (ticketNums <= 0) {
flag = false;
return;
} else {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "拿到" + ticketNums--);
}
}
}

3.同步块

  • 同步块:synchronize(obj) {}
  • Obj称为 同步监视器
    • Obj 可以是任何对象,但是推荐使用共享资源作为同步监视器。
    • 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,即这个对象本身,或者是class。
  • 同步监视器的执行过程:
    1. 第一个线程访问,锁定同步监视器,执行其中代码。
    2. 第二个线程访问,发现同步监视器被锁定,无法访问。
    3. 第一个线程执行完毕,解锁同步监视器。
    4. 第二个线程访问,发现同步监视器没有锁,然后锁定并访问。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SafeList {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
synchronized (list) {
list.add(Thread.currentThread().getName());
}
}).start();
}
Thread.sleep(3000);
System.out.println(list.size());
}
}

4.死锁

多个线程各自占有一些共享资源,并且相互等待其他线程占有的资源才能运行,而导致两个或多个线程都在等待对象释放资源,都停止执行的情形,某个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁问题”。

5.锁 Lock

  • 从JDK5.0开始,Java提供了更强大的线程同步机制—-通过显示定义同步锁对象来实现同步,同步锁使用Lock对象充当。
  • java.util.concurrent.locks.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能由一个对象对Lock对象加锁,线程开始访问共享资源之前应当获得Lock对象。
  • ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显示加锁、释放锁。
  • synchronized与Lock对比:
    • Lock是显示锁(手动开启和关闭锁),出了作用域自动释放。
    • Lock只有代码块锁定,synchronized有代码块和方法锁。
    • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,并且具有更好的扩展性。
    • 优先使用顺序: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
public class TestLock implements Runnable{
int ticketNums = 10;
private final ReentrantLock lock =new ReentrantLock();
@Override
public void run() {
while(true){
try{
lock.lock();
if (ticketNums>0){
Thread.sleep(1000);
System.out.println(ticketNums--);
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
TestLock lock = new TestLock();
new Thread(lock).start();
new Thread(lock).start();
new Thread(lock).start();
}
}

五、线程通信(生产者消费者模式)

生产>> 数据缓存区>>消费者

这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。

  • 对于生产者,没有生产产品之前,就要通知消费者等待。而生产了产品之后,又要马上通知消费者消费。
  • 对于消费者,在消费之后,要通知生产者已经消费结束,需要产生新的产品以供消费。
  • 在生产消费者问题中,仅有synchtonized是不够的。
    • synchronized可阻止并发更新同一共享资源,实现了同步。
    • synchronized不能用来实现不同线程之间的消息传递。
  • 并发协作模式 “生产者/消费者模式”==>管程法
    • 生产者:负责生产数据的模块。
    • 消费者:负责处理数据的模块。
    • 缓冲区:消费者不能直接使用生产者的数据,他们之间有个“缓冲区”。
    • 生产者将生产好的数据放入缓冲区,消费者从缓冲区中拿出数据。
  • 并发协作模式 “生产者/消费者模式”==>信号灯法,通过标志位。

1.管程法

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
66
67
68
69
70
71
72
73
74
public class TestPC {
public static void main(String[] args) {
SynContainer container = new SynContainer();
new Productor(container).start();
new Consumer(container).start();
}
}

@AllArgsConstructor
class Productor extends Thread {
SynContainer container;
@Override
public void run() {
for (int i = 0; i < 100; i++) {
container.push(new Chicken(i));
System.out.println("生产了第" + i + "只鸡");
}
}
}

@AllArgsConstructor
class Consumer extends Thread {
SynContainer container;
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("消费了第" + container.pop().id + "只鸡");
}
}
}

@AllArgsConstructor
class Chicken {
int id;
}
class SynContainer {
// 需要一个容器大小
Chicken[] chickens = new Chicken[10];
int count;
//生产者放入产品
public synchronized void push(Chicken chicken) {
// 如果容器满了,等待消费
if (count == chickens.length) {
// 通知消费者消费,生产者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果没满,则需要产生
chickens[count] = chicken;
count++;
this.notifyAll();
}

/**
* 消费者消费产品
*/
public synchronized Chicken pop() {
if (count == 0) {
// 等待生产者生产
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count--;
Chicken chicken = chickens[count];
this.notifyAll();// 通知
return chicken;
}
}

2.标志位法

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
66
67
68
public class TestPc2 {
public static void main(String[] args) {
Tv tv = new Tv();
new Player(tv).start();
new Watcher(tv).start();
}
}

// 生产者 演员
@AllArgsConstructor
class Player extends Thread {
Tv tv;
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
this.tv.play("11111111111");
} else {
this.tv.play("222222222222");
}
}
}
}

// 消费者 观众
@AllArgsConstructor
class Watcher extends Thread {
Tv tv;
@Override
public void run() {
for (int i = 0; i < 20; i++) {
tv.watch();
}
}
}

class Tv {
String voice;//表演节目
boolean flag = true;//标志位
// 表演
public synchronized void play(String voice) {
if (!flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("演员表演了:" + voice);
// 通知观众
this.notifyAll();
this.voice = voice;
this.flag = !this.flag;
}
// 观看
public synchronized void watch() {
if (flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("观看了:" + voice);
this.notifyAll();
this.flag = !this.flag;
}
}

六、线程池

  • 背景:经常创建和销毁线程,使用了很多资源。比如在并发情况下的线程,对性能影响很大。
  • 思路:提前创建好多个线程,放入线程池中,使用时直接获取,并使用完放回池中,可以避免频繁创建销毁、实现重复利用。
  • 好处:提高相应速度(减少创建新线程的时间)、降低资源消耗(重复利用线程池中线程,不需要每次都创建)、便于线程管理(corePoolSize:核心池大小,maximumPoolSize:最大线程数,keepAliveTime:线程没有任务时最多保持多长时间后会终止。)
  • JDK5提供了线程池相关API:ExecutorServiceExecutors
  • ExecutorService:真正的线程池接口,常见的子类ThreadPoolExecutor
    • void execute(Runnable command):执行任务,没有返回值,一般用于执行Runnable
    • <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般用于执行Callable
    • void shutdown():关闭连接池
  • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TestPool {
public static void main(String[] args) {
//创建线程池
ExecutorService service = Executors.newFixedThreadPool(10);
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
// 2. 关闭连接
service.shutdown();
}
}
class MyThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() );
}
}
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 MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 200; i++) {
System.out.println(Thread.currentThread().getName() + "线程运行中!");
}
return "子线程执行完毕";
}

public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建执行服务
ExecutorService service = Executors.newFixedThreadPool(3);
// 提交执行
Future<String> r1 = service.submit(new MyCallable());
Future<String> r2 = service.submit(new MyCallable());
Future<String> r3 = service.submit(new MyCallable());
//获取执行结果
String s1 = r1.get();
String s2 = r2.get();
String s3 = r3.get();
//关闭服务
service.shutdown();
}
}

七、其他知识

  1. synchronized锁定的是当前对象。

  2. 使用synchronized锁定静态方法、静态变量,即表示锁定Class对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class T {

    private static int count = 10;

    public synchronized static void m() { //这里等同于synchronized(T.class)
    count--;
    System.out.println(Thread.currentThread().getName() + " count = " + count);
    }
    public static void mm() {
    //这里不能携程synchronized(this),因为静态方法不需要创建对象,所以没有this
    synchronized(T.class) {
    count --;
    }
    }
    }
  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
    public class T {
    public synchronized void m1() {
    System.out.println(Thread.currentThread().getName() + " m1 start...");
    try {
    Thread.sleep(10000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + " m1 end");
    }
    public void m2() {
    try {
    Thread.sleep(5000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + " m2 ");
    }
    public static void main(String[] args) {
    T t = new T();
    new Thread(t::m1, "t1").start();
    new Thread(t::m2, "t2").start();
    }
    }
  4. 脏读:在写入的方法上加锁,读出的方法上不加锁。读到在写中还没完成的数据。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Account {
    String name;
    double balance;
    public synchronized void set(String name, double balance) {
    this.name = name;
    this.balance = balance;
    }
    public double getBalance(String name) {
    return this.balance;
    }
    }
  5. 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
    // 1. 同一对象中
    public class T {
    synchronized void m1() {
    System.out.println("m1 start");
    m2();
    }
    synchronized void m2() {
    System.out.println("m2");
    }
    }
    // 2. 子类调用父类的同步方法
    public class T {
    synchronized void m() {
    System.out.println("m start");
    Thread.sleep(1000);
    System.out.println("m end");
    }
    }
    class TT extends T {
    @Override
    synchronized void m() {
    System.out.println("child m start");
    super.m();
    System.out.println("child m end");
    }
    public static void main(String[] args) {
    new TT().m();
    }
    }
    // child m start
    // m start
    // m end
    // child m end
  6. 程序在执行过程中,如果出现异常,默认情况锁会被释放。所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。比如,在一个web处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适,在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。因此要非常小心的处理同步业务逻辑中的异常。

    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 T {
    int count = 0;
    synchronized void m() {
    System.out.println(Thread.currentThread().getName() + " start");
    while(true) {
    count ++;
    System.out.println(Thread.currentThread().getName() + " count = " + count);
    if(count == 5) {
    int i = 1/0; //此处抛出异常,锁将被释放,要想不被释放,可以在这里进行catch,然后让循环继续
    System.out.println(i);
    }
    }
    }
    public static void main(String[] args) {
    T t = new T();
    Runnable r = new Runnable() {
    @Override
    public void run() {
    t.m();
    }
    };
    new Thread(r, "t1").start();
    new Thread(r, "t2").start();
    }
    }
  7. volatile 关键字,使一个变量在多个线程间可见。A和B线程都用到一个变量,Java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道。使用volatile关键字,会让所有线程都会读到变量的修改值。

    volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class T {
    volatile boolean running = true;
    void m() {
    System.out.println("m start");
    while(running) {}
    System.out.println("m end!");
    }
    public static void main(String[] args) {
    T t = new T();
    new Thread(t::m, "t1").start();
    Thread.sleep(1000);
    t.running = false;
    }
    }