多线程与高并发编程基础知识

基础概念

进程、线程的介绍

进程相对于程序来说是一个动态的概念,如:QQ、微信等是一个程序,当双击程序时,程序运行起来就是一个进程。线程作为进程中一个最小的执行单元,线程(Thread)就是一个程序中不同的执行路径,用如下代码对线程进行说明:
public class ThreadPractise {
private static class T1 extends Thread {
@Override
public void run() {
for (int i=0;i<10;i++){
try {
TimeUnit.MICROSECONDS.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(“T1线程”);
}
}
}

public static void main(String[] args) {
    //启动线程的方式1
    new T1().run();
    //启动线程的方式2
    new T1().start();
    for(int i=0;i<10;i++){
        try {
            TimeUnit.MICROSECONDS.sleep(1);
        }catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main线程");
    }
}

}
run方式:直接调用重写后的run方法,运行结果如下:
在这里插入图片描述
可以看到,程序启动时,先调用T1的run方法,执行完该方法之后,然后执行了main方法中的输出内容。run方法的调用相当于main方法开始执行时,先执行T1的run方法,执行完之后再回到main方法,此时程序中只有一条执行路径。

start方式:调用父类Thread的start方法,运行结果如下:
在这里插入图片描述
可以看到,输出的结果为T1的方法和main方法交替输出。原因是main方法执行时,当调用start方法时,会产生一个分支,这个分支会和主程序一起运行,这就是一个程序中不同的执行路径,不同的线程在同时运行。

启动线程的方式

通过如下代码进行说明:
在这里插入图片描述
注意:1)调用的是start方法,不是run方法!2)通过实现Runnable接口来创建线程时,要想让线程运行起来,必须先new Thread,然后将实现Runnable接口的类传进去,然后调用start方法。

关于线程的一个面试题

面试题:启动线程的三种方式。
解答:1)通过继承Thread;2)实现Runnable;3)通过Executors.newCachedThreadPool()拿到线程池,然后启动线程,在线程池中启动线程其实用的也是前两种方式。

线程的基本方法

1.sleep:当前线程暂停一段时间,让别的线程来运行。从操作系统角度来讲,CPU中是没有线程的概念的,其内部是不断的死循环,从内存中取指令执行,一直循环,没有指令的话,就歇着,所以对CPU来说,是没有线程的概念的。而多线程是指,如果只有一个CPU的话,有许多不同的线程,每个线程在CPU上执行一会,执行完将当前线程扔出去,将下一个线程哪进来。
2.yield:A线程在运行,B线程也在运行,A线程在CPU上运行时调用了yield方法,然后它会谦让的先离开CPU,此时别的线程可以在CPU上执行,当然如果没有线程执行,A还是会再回来执行的,所谓的离开就是进入到一个等待队列中。
3.join:加入的意思。如果有T1、T2俩线程,如果是在T1的某个点上调用了T2.join(说明:在自己的线程中调用join是没有意义的),此操作的意思是跑到T2上去运行,T1在等着,什么时候T2运行完了,再运行T1,相当于把T2线程加入到T1线程中。join方法经常用来等待另外一个线程的结束。
关于join方法的面试题:现有三个线程:T1、T2、T3,如何才能保证三个线程按顺序执行完。
解答:1)主线程起来之后,先调用T1.join,再2调用T2.join,最后调用T3.join;2)在T1线程中调用T2.join,T2线程中调用T3.join,保证T3先执行完,然后是T2,最后是T1。

线程的六个状态

如下图所示:
在这里插入图片描述
new状态:当new Thread后,但是还没有调用start方法时,就是new状态。
Runnable状态:当调用start时,会被线程调度器来执行(也就是交给操作系统来执行),操作系统执行时,整个状态是Runnable,内部有两个状态:Ready(就绪状态)和Running(运行状态),就绪状态是指扔到CPU的等待队列中,真正在CPU上运行时是Running状态。
Terminated状态:当一切顺利执行完之后,进入此状态。注意:当执行完Terminated时,不可以再调用start方法,这是不被允许的!
以下是Runnable状态的变迁:
TimedWaiting状态:按照时长来等待,在运行时,如果调用了Object的wait(time)方法/Thread的join(time)方法/LockSupport 的parkNanos()方法/LockSupport的parkUtil()()方法,进入了TimedWaiting状态,超过设置时间,自动从阻塞状态回到Runnable状态。
Waiting状态:在运行时,如果调用了Object的wait方法/Thread的join方法/LockSupport的park方法,进入了Waiting状态,通过Object的notify方法/Object的notifyAll方法方法/LockSupport的unpark方法重新回到Runnable状态。
Blocked阻塞状态:加上同步代码块,进入代码块中没有得到锁的时候,进入阻塞状态。获得锁的时候,进入就绪状态,去运行。
说明:1)这些状态都是有JVM管理的,JVM管理时要通过操作系统,JVM是跑在操作系统上的一个普通的程序。
2)可以通过调用线程的getState()方法来查看线程的状态。

interrupt介绍

当我们调用Object的wait方法/Thread的join方法/LockSupport的park方法时都有可能被interrupt(打断),被打断之后会抛出InterruptException,需要在原来的程序中catch InterruptException异常,这里需要注意的是:并不是依赖interrupt方法之后,程序就被打断了,而是当你catch到这个异常后具体的处理,如果你决定停止,那程序就停止;如果你决定catch到异常后该干嘛干嘛,那程序继续运行。

浅谈stop方法和interrupt方法

stop方法:在工程中,该方法已经被废弃,不建议使用。
interrupt方法:在框架源码用来控制程序的业务逻辑流程,Netty源码和JDK锁的源码用到过。我们写代码时一般很少用,当我们起了一个等待时间非常长甚至是阻塞状态的操作,如:读网络上传来的一个包,包不来就一直停着。这时我们想关闭整个程序,如何通知这个正在等待的线程呢?如果线程处于wait,可以通过notify来通知正在等待的线程;如果是线程是sleep状态,调用interrupt来唤醒线程,此时需要catch InterruptException。

synchronized关键字

synchronized关键字既保证了多个线程操作时的原子性,又保证了可见性,但是不能防止指令重排序(下面会介绍)。用下图来说明为什么多个线程访问同一个资源时,需要上锁:
在这里插入图片描述
误区:我们使用synchronized的时候,到底是对谁进行了锁定?
如下所示:
在这里插入图片描述
注意:这里不是对非得要访问的对象进行锁定,是你想锁谁就可以锁谁,只是当你要执行相应代码时,先要拿到锁对象。如:

public class SynchronizedTest {
   
    public class T {
   
        private int count = 10;
        Object o = new Object();//这里锁的是Object对象
        public void m(){
   
            synchronized (o){
   //任何线程要想执行下面代码,必须先要拿到o的锁
                count--;
                System.out.println(Thread.currentThread().getName()+"count"+count);
            }
        }
    }
}
注意:给某个对象上锁时,该对象不能用String常量、IntegerLong等数据类型!
分析:所有用在字符串常量的地方,其实都是用的同一个(String底层是用final修饰的),假如第一个线程锁定了字符串常量,第二个线程又尝试锁字符串常量,其实第二个线程锁的跟第一个线程是同一个对象,另外如果这俩线程是同一个,那就重入了,但是重入之后不一定是你想要的结果,如果不是同一个线程,就可能造成死锁。而Integer内部进行了处理,当new出来的Integer对象只要值改变了,它就会产生新对象,此时锁对象就改变了。
如果每次加锁的时候都要new一个对象的话,太麻烦了,所以可以用synchronized(this)
来锁定当前对象,也可以在方法上加synchronized,这两种方式是等值的。如:
```java
public class SynchronizedTest1 {
   
    public class T {
   
        private int count = 10;
        //方式一:方法上加锁
        public synchronized void m(){
   
            count--;
            System.out.println(Thread.currentThread().getName()+"count"+count);
        }
        //方式二:锁this
        public void n(){
   
            synchronized (this){
   
                count--;
                System.out.println(Thread.currentThread().getName()+"count"+count);
            }
        }
    }
}
如果是被static修饰的静态资源,是没有this对象的,那么加了锁后,它锁的是谁呢?用如下代码来解释:
```java
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);
        }
}

每一个class文件load到内存中,会生成一个class类的字节码对象,和load到内存的代码相对应。这里synchronized(T.class)锁的就是T的class字节码对象。
思考1:这里T.class在load到内存中是不是单例的呢?
结论分析:一般情况下是单例的。如果是在同一个classLoad(类加载器)空间中,就是单例的,如果load到不同的classLoad空间中,就不是单例,同一个进程中可以有多个classLoad,classLoad是可以自定义的,但是不同的classLoad互相是不能访问的,所以只要能访问,那一定是单例的。
思考2:锁定的必须是同一个对象吗?
结论分析:是的,必须是同一个对象。如果两次锁的对象不同,那么这两个对象不能构成互斥,不是互斥的锁是没有意义的。

关于同步方法和非同步方法能否同时执行的小面试题

面试题:模拟银行账户操作业务
前提:该银行允许用户出现脏读(就是在还没有set完的时候读取数据)
要求:对业务写方法进行加锁;对业务读方法不加锁
可以参考如下代码:

public class Account {
   
    private String name;
    private double balance;
    public synchronized void set(String name,double balance){
   
        this.name = name;
        //模拟场景:写方法sleep,读方法执行
        try {
   
            Thread.sleep(2000);
        }catch (InterruptedException e){
   
            e.printStackTrace();
        }
        this.balance = balance;
    }
    public /*synchronized*/ double get(String name){
   
        return this.balance;
    }
    public static void main(String[] args) {
   
        Account a = new Account();
        //启动线程
        new Thread(()->a.set("zhangsan",100.00)).start();
        try {
   
            TimeUnit.SECONDS.sleep(1);
        }catch (InterruptedException e){
   
            e.printStackTrace();
        }
        //写方法sleep的时候,用户读数据,因为set方法睡了2秒钟,读线程睡了1秒后得到的数据是还没有set完的数据
        System.out.println(a.get("zhangsan"));
        try {
   
            TimeUnit.SECONDS.sleep(2);
        }catch (InterruptedException e){
   
            e.printStackTrace();
        }
        System.out.println(a.get("zhangsan"));
    }
}

结论:锁定方法(同步方法)和非锁定方法(非同步方法)可以同时执行。

synchronized的属性—可重入性

可重入:同一个线程,如果一个同步方法调另一个同步方法,如:方法m1加了锁,另一个方法m2也加了锁,而且加的是同一把锁,此时m1方法中调用m2,同样m2方法也调了m1,如果synchronized是不可重入的,那么m1中调m2时,就会发生死锁,因为不是可重入的话,是不允许访问的,方法进行不下去。如果是可重入的,执行到m2方法时,会发现它加的锁跟m1自己的一样,就会去执行m1方法。还有一个例子:如果一个子类重写了父类的某个同步(加了锁)方法,在重写的方法中调用super.同步方法,如果synchronized是不可重入的,那么在父子类之间就发生了死锁。
注意:1)程序执行过程中,如果出现异常,默认情况下锁会被释放。
2)在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。比如:在一个web app处理过程中,多个业务层的线程共同访问一个资源,这时如果异常处理不合适,在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。因此要非常小心的处理同步业务逻辑中的异常。

补充:同步代码块说明

为了解决并发操作可能造成的异常,java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块,其语法如下:
synchronized(要锁定的对象){
// 同步代码块
}
其中obj就是同步监视器,它的含义是:线程开始执行同步代码块之前,必须先获得对同步代码块的锁定。任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。虽然java程序允许使用任何对象作为同步监视器,但是同步监视器的目的就是为了阻止多个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器。

synchronized锁升级的概念

锁升级的概念:早期的synchronized是重量级的(就是需要向操作系统申请锁),到JDK1.5之后,对synchronized进行了改进,后来当我们使用synchronized的时候,Hotspot(Oracle对JVM规范的实现)的实现是:当我们访问上了锁的线程时(如:sync(Object)),底层实际上没有直接加锁,而是先在Objeect “头上” markword(JVM中的知识点) 做个标记,表示这个线程的ID(偏向锁),如果下次还是这个线程访问,直接进入,这样效率非常高,这种情况下通常是一个线程在执行;如果不是这个线程,此时就会有线程的争用,这时候偏向

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值