JAVA多线程与高并发(一)[线程概念,同步synchronize关键字]

释放双眼,带上耳机,听听看~!

link-JAVA多线程与高并发系列[前言,大纲]

线程

程序,进程,线程,协成/纤程(fiber)

举个例子:
程序:QQ.exe是一个程序,存放在硬盘上,是一个静态的概念;
进程:当你双击它,QQ程序运行起来了,这就是一个进程.相对程序来说,进程是一个动态的概念;
线程:进程中最小的执行单元,一个进程内可以有多个线程同时执行命令

在JAVA中创建一个线程:
如果调用T1的run()方法,那就是简单的方法调用,代码还是在main线程中从上到下依次执行;
如果调用T1的start()方法,则会开启一个新的线程(记为thread1)执行,那么thread1和main线程同时执行,表现为控制台"T1"和"main"交替输出.


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
1public class T01_WhatIsThread {
2    private static class T1 extends Thread {
3        @Override
4        public void run() {
5           for(int i=0; i<10; i++) {
6               try {
7                   TimeUnit.MICROSECONDS.sleep(1);
8               } catch (InterruptedException e) {
9                   e.printStackTrace();
10               }
11               System.out.println("T1");
12           }
13        }
14    }
15
16    public static void main(String[] args) {
17        //new T1().run();
18        new T1().start();
19        for(int i=0; i<10; i++) {
20            try {
21                TimeUnit.MICROSECONDS.sleep(1);
22            } catch (InterruptedException e) {
23                e.printStackTrace();
24            }
25            System.out.println("main");
26        }
27
28    }
29}
30
31

创建线程的3种方式?

1:Thread 2: Runnable 3:Executors.newCachedThrad


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
1public class T02_HowToCreateThread {
2    static class MyThread extends Thread {
3        @Override
4        public void run() {
5            System.out.println("Hello MyThread!");
6        }
7    }
8
9    static class MyRun implements Runnable {
10        @Override
11        public void run() {
12            System.out.println("Hello MyRun!");
13        }
14    }
15
16    public static void main(String[] args) {
17        // 第一种,对象继承Thread,然后直接new该对象
18        new MyThread().start();
19        // 第二种,对象实现Runnable,该对象作为参数创建一个Thread对象
20        new Thread(new MyRun()).start();
21        // 第二种的变式,其实是匿名内部类的实例作为实现Runnable的对象,又加了lambda表达式简化代码
22        new Thread(()->{
23            System.out.println("Hello Lambda!");
24        }).start();
25        // 第三种线程池Executors.newCachedThrad等,其实最终实现也是前两种之一
26    }
27
28}
29
30

线程的常见操作

前言

CPU只管执行命令,对于CPU来说,没有线程的概念,它只是不断的从内存中去拿取指令去执行.
多线程呢,就是有好多个线程竞争着去给CPU发送命令,每个线程上的命令在CPU上执行一小会,多个线程的命令快速交替执行,这样看起来像是同时执行的.
各个线程和CPU之间,相当于有一个"等待队列",线程们在队列中排队,等着CPU从队列中随机找一个线程去执行;CPU执行某个线程的一小段命令后,不等执行完,就把它扔回等待队列,然后重新找一个线程执行,这样快速的切换感觉像是多个线程同时执行.如果线程执行完了,那就终结了自己的一生,不会再进入等待队列了.
线程操作:

  1. sleep(long millis):当前线程休息一定毫秒数,把执行命令的机会让给其他线程,睡够一定时间后进入等待队列,继续竞争CPU的执行机会
  2. yield():当前线程正在CPU上运行时,先退出一下,进去线程等待队列,然后大家再一起公平竞争CPU的执行机会.这个几乎用不到.
  3. join():假设俩个线程t1,t2,在t1中调用t2.join(),则此时执行t2的命令,t2执行完后t1继续执行,用于保证线程的执行顺序
  4. stop(): 在工程中尽量不要用,容易出现状态不一致问题,略复杂,就当没有这个方法吧
  5. interrupt():interrupt后会抛出一个异常,需要在上层catch改异常,然后做一些逻辑处理,控制程序流程.业务逻辑中也几乎没有必须interrupt的,也尽量不要用.

假想的一个interrupt的场景:线程t1调用了sleep(两天)的方法,但是在一天后,需要让它醒来,那就调用t1.interrupt();前提是t1sleep时做好被interrupt的准备,即catchinterrupted异常,然后继续执行或者干点别的事.

线程的常见状态

线程状态迁移图:
线程在"等待队列"中等着被CPU执行时,就是Ready状态;线程正在被CPU执行时,是Running状态;Ready和Running合称为Runnable状态.
线程执行完后(或者被操作系统kill掉),进入terminated状态,结束了自己的一生,啥都不能干了(等待被GC回收).
获取线程状态的方法:new MyThead().getState();

同步

synchronize关键字(常见问题)

多个线程去访问同一个资源的时候,需要上锁,目的是保证状态的一致,就像数据库的事务一样.
类比场景:多个同学去厕所蹲坑.

锁的是什么?

是一个对象(包括class对象),拿到锁之后才能去执行某段代码.而不是锁的代码.

注意不要用String常量和Integer等基础数据类型作为锁的对象

不能用String的原因:所有的字符串常量都是同一个对象,假如引用的依赖包中用了String常量作为锁的对象,自己的程序也刚好用了相同String常量作为锁的对象,那么肯定会引起奇怪的问题.
也尽量不用String对象.
不能用Integer等基础数据类型的原因:Integer内部做了一些特殊处理,Integer的对象的值一旦改变,就会变成一个新对象


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1public class T {
2  
3   private int count = 10;
4   private Object o = new Object();
5  
6   public void m() {
7       synchronized(o) { //任何线程要执行线面的代码,必须先拿到对象o的锁
8           count--;
9           System.out.println(Thread.currentThread().getName() + " count = " + count);
10      }
11  }
12 
13}
14
15

synchronize底层怎么实现?什么是锁升级?synchronize一定比原子类慢?

JVM规范中没有任何要求,只要能保证功能完整就行.
HotSpot是这样的:对象头(64位)中,拿出2位来记录这个对象是不是被锁定了,mark word.

  1. JDK早期,synchronize是重量级的,每次都去找操作系统申请锁,效率很低;
  2. 后来改进了,引入了锁升级(可以看没错,我就是厕所所长一文)

2.1. 偏向锁:当第一个线程去执行带synchronize方法时,先在对象头的markword上记录这个线程的线程号,不加锁;如果下次又是该线程执行那个方法,就直接访问不需获取锁.(偏向第一个线程)
2.2. 自旋锁:如果有线程争用,则升级为自旋锁,比如线程t1正在访问带锁资源r,这时t2也要访问r,那么t2先不加锁,while(true)空执行一会,看t1是不是会马上释放锁.默认自旋10次,如果10后还得不到锁,则升级为重量级锁
2.3. 重量级锁:去操作系统申请资源加锁
Hotspot目前的实现,锁只能升级,不能降级.
所以现在的synchronize并不一定比那些原子类慢,因为有锁升级
自旋锁占用CPU,但是不去跟操作系统申请资源加锁,只是在用户态,不经过内核态.
当加锁方法执行时间很长,或者线程数很多时,用操作系统锁比较好;
当执行时间很短,且线程不太多时,用自旋锁合适.

synchronize的特点

  1. 可以在方法上加synchronize关键字,锁定当前对象(非静态方法),或者当前类的class对象(静态方法)
  2. synchronize(this)锁定当前对象;
  3. 对于非静态方法,synchronize(this)如果锁住了方法中的所有代码,那就和直接在方法上加synchronize是一样的;对于静态方法,方法上的synchronize相当于synchronize(T.class)

(每一个.class文件,load到内存以后,会生成一个对应的Class对象)

  1. synchronize既保证可见性,又保证原子性
  2. 可重入.假如两个方法m1,m2都对同一个对象加了锁;如果m1中调用了m2,是可以再次获得锁的.(如果不允许重入,那就死锁了).可重入的概念是在同一个线程的基础上的.

class对象是单例的吗?

同一个ClassLoader内是单例的;多个ClassLoader间,不是单例;但是不同加载器之间不能互相访问.所以,可以认为是单例.

父类中有一个synchronize方法,在子类中调用,那么锁的是谁?

是子类对象,打印一下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
1public class T {
2   synchronized void m() {
3       System.out.println("super m start");
4       System.out.println(this);
5       try {
6           TimeUnit.SECONDS.sleep(1);
7       } catch (InterruptedException e) {
8           e.printStackTrace();
9       }
10      System.out.println("super m end");
11  }
12 
13  public static void main(String[] args) {
14      new TT().m();
15      System.out.println("---");
16      new TT().m2();
17      System.out.println("---");
18      new TT().m3();
19  }
20 
21}
22
23class TT extends T {
24  @Override
25  synchronized void m() {
26      System.out.println("child m start");
27      System.out.println(this);
28      super.m();
29      System.out.println("child m end");
30  }
31
32  synchronized void m2() {
33      System.out.println("child m2 start");
34      System.out.println(this);
35      super.m();
36      System.out.println("child m2 end");
37  }
38
39  void m3() {
40      System.out.println("child m2 start");
41      System.out.println(this);
42      super.m();
43      System.out.println("child m2 end");
44  }
45}
46
47

带锁的方法和不带锁的方法可以同时执行吗?

可以.

程序中如果抛出了异常,锁会被释放吗?

抛出异常会释放锁,所以一定要处理好异常,防止出现异常后被其他线程访问资源,从而导致各种状态不一致的问题.

锁优化

锁细化:锁住(synchronize包括)的代码,在能保证业务逻辑OK下,越少越好.
锁粗化:假如一段业务逻辑,中间有很多个细化的小锁,这些小锁的方法别的业务又不会调用,那就把这些小锁合并成一个大锁.

锁的属性(field)发生变化,会影响锁的使用效果吗?

锁的属性变化不会影响锁的功能;
但是如果锁的变量(或者说"引用")指向了别的对象,那就不是同一把锁了,会出问题.可以通过给变量加上final关键字避免这个问题.

给TA打赏
共{{data.count}}人
人已打赏
安全经验

英文站如何做Google Adsense

2021-10-11 16:36:11

安全经验

安全咨询服务

2022-1-12 14:11:49

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索