<small id='JWYSkU0LxV'></small> <noframes id='V1ZX'>

  • <tfoot id='JP7rF6qxv'></tfoot>

      <legend id='tlV54y'><style id='N7AgroCM'><dir id='5ElI'><q id='0k45SKMvQ'></q></dir></style></legend>
      <i id='VujZh1HW'><tr id='CRKu5S'><dt id='AWuYO'><q id='Q6Tspgq'><span id='YebfXOz'><b id='3AG56fs1'><form id='09RmtL'><ins id='JCV21'></ins><ul id='P1HGFZdJUC'></ul><sub id='kd8zG93Nm'></sub></form><legend id='RTiM0a5zL7'></legend><bdo id='XYZdCFP1EU'><pre id='yzrbn5Z'><center id='eXyNcC2zr'></center></pre></bdo></b><th id='7wWc85n'></th></span></q></dt></tr></i><div id='rucOQlT6k'><tfoot id='f24KvJ9Op0'></tfoot><dl id='KJUEWPR'><fieldset id='mUjiM'></fieldset></dl></div>

          <bdo id='F4IDx5uQSZ'></bdo><ul id='m60YqxXur'></ul>

          1. <li id='uT9pdH'></li>
            登陆

            阿里后端面试题篇五——Java程序员必备:多线程、锁

            admin 2019-11-19 243人围观 ,发现0个评论

            1、完成多线程的两种办法

            完成多线程有两种办法:承继Thread和完成Runnable接口。

            承继Thread:

            以卖票为例:

            public class MyThread extends Thread {
            private static int COUNT = 5;
            private int ticket = COUNT;
            private String name;
            public MyThread(String s){
            name = s;
            }
            @Override
            public void run() {
            for (int i = 0; i < COUNT; i++){
            if(ticket > 0){
            System.out.println(name + "-->" + ticket--);
            }
            }
            }

            测验运用:

            MyThread thread1 = new MyThread("thread1");
            MyThread thread2 = new MyThread("thread2");
            thread1.start();
            thread2.start();

            输出:

            thread1-->5
            thread2-->5
            thread1-->4
            thread2-->4
            thread1-->3
            thread2-->3
            thread1-->2
            thread2-->2
            thread1-->1
            thread2-->1

            能够看到,这种办法每个线程自己具有了一份票的数量,没有完成票的数量同享。下面看完成Runnable的办法:

            完成Runnable接口:

            public class MyRunnable implements Runnable {
            private static int COUNT = 5;
            private int ticket = COUNT;
            @Override
            public void run() {
            for(int i = 0; i < COUNT; i++){
            if(ticket > 0){
            System.out.println("ticket-->" + ticket--);
            }
            }
            }
            }

            测验运用:

             MyRunnable runnable = new MyRunnable();
            new Thread(runnable).start();
            new Thread(runnable).start();

            输出:

            ticket-->5
            ticket-->3
            ticket-->2
            ticket-->1
            ticket-->4

            能够看到,完成Runnable的办法能够完成同一资源的同享。

            实践工作中,一般运用完成Runnable接口的办法,是由于:

            • 支撑多个线程去处理同一资源,一起,线程代码和数据有用别离,表现了面向目标的思维;
            • 避免了Java的单承继性,假如运用承继Thread的办法,那这个扩展类就不能再去承继其他类。

            拓宽

            Thread的start()和run()办法差异:

            start()办法用于发动一个线程,使其处于安排妥当状况,得到了CPU就会履行,而直接调用run()办法,就适当所以一般的办法调用,会在主线程中直接运转,此刻没有敞开一个线程。

            下列办法中哪个是履行线程的办法? ()

            • A、run()
            • B、start()
            • C、sleep()
            • D、suspend()

            正确答案:A

            • run()办法用来履行线程体中详细的内容
            • start()办法用来发动线程目标,使其进入安排妥当状况
            • sleep()办法用来使线程进入睡觉状况
            • suspend()办法用来使线程挂起,要经过resume()办法使其从头发动

            2、拜访操控润饰符(新弥补)

            关于拜访操控润饰符,在第一篇总结中已有详细的介绍。但最近在运用String类的一个办法compareTo()的时分,对private润饰符有了新的了解。String类的compareTo办法是用来比较两个字符串的字典序的,其源码如下:

            public int compareTo(String anotherString) {
            int len1 = value.length;
            int len2 = anotherString.value.length;
            //重点是这儿!!!
            int lim = Math.min(len1, len2);
            char v1[] = value;
            char v2[] = anotherString.value;
            //重点是这儿!!!
            int k = 0;
            while (k < lim) {
            char c1 = v1[k];
            char c2 = v2[k];
            if (c1 != c2) {
            return c1 - c2;
            }
            k++;
            }
            return len1 - len2;
            }

            上面代码逻辑很好了解,我在看到它里边直接运用anotherString.value来获取String的字符数组的时分很古怪,由于value是被界说为private的,只能在类的内部运用,不能在外部经过类目标.变量名的办法拜访。咱们往常都是经过String类的toCharArray()办法来获取String的字符数组的,看到上面的这种运用办法,我赶忙在其他当地测验了一下,发现的确是不能直接经过xx.value的办法来获取字符数组。

            正如前面所说的,value是被界说为private的,只能在类的内部运用,不能在外部经过类目标.变量名的办法拜访。由于compareTo办法便是String类的内部成员办法,compareTo办法的参数传递的便是String目标过来,此刻运用“类目标.变量名”的办法是在该类的内部运用,因而能够直接拜访到该类的私有成员。自己再仿照String类来测验一下,发现不出所料。。

            问题很细节,可是没有一下想通,阐明仍是对private的润饰符了解不行到位,前面自认为只要是private润饰的,就不能经过“类目标.变量名”的办法拜访,其实仍是需求看在哪里边运用。

            3陈细妹、线程同步的办法

            当咱们有多个线程要拜访同一个变量或目标时,而这些线程中既有对改变量的读也有写操作时,就会导致变量值呈现不行预知的状况。如下一个取钱和存钱的场景:

            没有参加同步操控的景象:

            public class BankCount {
            private int count = 0;
            //余额
            public void addMoney(int money){
            //存钱
            count += money;
            System.out.println(System.currentTimeMillis() + "存入:" + money);
            System.out.println("账户余额:" + count);
            }
            public void getMoney(int money){
            //取钱
            if(count - money < 0){
            System.out.println("余额缺乏");
            System.out.println("账户余额:" + count);
            return;
            }
            count -= money;
            System.out.println(System.currentTimeMillis() + "取出:" + money);
            System.out.println("账户余额:" + count);
            }
            }

            测验类:

            public class BankTest {
            public static void main(String[] args) {
            final BankCo阿里后端面试题篇五——Java程序员必备:多线程、锁unt bankCount = new BankCount();
            new Thread(new Runnable() {
            //取钱线程
            @Override
            public void run() {
            while(true){
            bankCount.getMoney(200);
            try {
            Thread.sleep(1000);
            }
            catch (InterruptedException e) {
            e.printStackTrace();
            }
            }
            }
            }
            ).start();
            new Thread(new Runnable() {
            //存钱线程
            @Override
            public void run() {
            while(true){
            bankCount.addMoney(200);
            try {
            Thread.sleep(1000);
            }
            catch (InterruptedException e) {
            e.printStackTrace();
            }
            }
            }
            }
            ).start();
            }
            }

            部分打印成果如下:

            余额缺乏
            账户余额:0
            1462265808958存入:200
            账户余额:200
            1462265809959存入:200
            账户余额:200
            1462265809959取出:200
            账户余额:200
            1462265810959取出:200
            账户余额:200
            1462265810959存入:200
            账户余额:200
            1462265811959存入:200
            账户余额:200

            能够看到,此刻有两个线程一起运用操作了bankCount目标中的count变量,使得count变量成果不契合预期。因而需求进行同步操控,同步操控的办法有以下几种:

            (1)运用synchronized关键字同步办法

            每一个Java目标都有一个内置锁,运用synchronized关键字润饰的办法,会运用Java的内置锁作为锁目标,来保护该办法。每个线程在调用该办法前,都需求取得内置锁,假如该锁已被其他线程持有,当时线程就进入堵塞状况。

            修正BankCount 类中的两个办法,如下:

            public synchronized void addMoney(int money){
            //存钱
            count += money;
            System.out.println(System.currentTimeMillis() + "存入:" + money);
            System.out.println("账户余额:" + count);
            }
            public synchronized void getMoney(int money){
            //取钱
            if(co阿里后端面试题篇五——Java程序员必备:多线程、锁unt - money < 0){
            System.out.println("余额缺乏");
            System.out.println("账户余额:" + count);
            return;
            }
            count -= money;
            System.out.println(System.currentTimeMillis() + "取出:" + money);
            System.out.println("账户余额:" + count);
            }

            运转测验打印如下成果:

            余额缺乏
            账户余额:0
            1462266451171存入:200
            账户余额:200
            1462266452171取出:200
            账户余额:0
            1462266452171存入:200
            账户余额:200
            1462266453171存入:200
            账户余额:400
            1462266453171取出:200
            账户余额:200
            1462266454171存入:200
            账户余额:400
            1462266454171取出:200
            账户余额:200
            1462266455171取出:200
            账户余额:0

            能够看到,打印成果契合咱们的预期。

            别的,假如咱们运用synchronized关键字来润饰static办法,此刻调用该办法将会锁住整个类。(关于类锁、目标锁下面有介绍)

            (2)运用synchronzied关键字同步代码块

            运用synchronized关键字润饰的代码块,会运用目标的内置锁作为锁目标,完成代码块的同步。

            改造BankCount 类的两个办法:

            public void addMoney(int money){
            //存钱
            synchronized(this){
            count += money;
            System.out.println(System.currentTimeMillis() + "存入:" + money);
            System.out.println("账户余额:" + count);
            }
            }
            public void getMoney(int money){
            //取钱
            synchronized(this){
            if(count - money < 0){
            System.out.println("余额缺乏");
            System.out.println("账户余额:" + count);
            return;
            }
            count -= money;
            System.out.println(System.currentTimeMillis() + "取出:" + money);
            System.out.println("账户余额:" + count);
            }
            }

            (注:这儿改造后的两个办法中由于synchronized包括了办法体的整个代码句子,功率上与在办法名前加synchronized的第一种同步办法差不多,由于里边触及到了打印money仍是需求同步的字段,所以悉数包括起来,仅仅是为了阐明synchronized作用...)

            打印成果:

            余额缺乏
            账户余额:0
            1462277436178存入:200
            账户余额:200
            1462277437192存入:200
            账户余额:400
            1462277437192取出:200
            账户余额:200
            1462277438207取出:200
            账户余额:0
            1462277438207存入:200
            账户余额:200
            1462277439222存入:200
            账户余额:400
            1462277439222取出:200
            账户余额:200

            能够看到,履行成果也契合咱们的预期。

            synchronized同步办法和同步代码块的挑选:

            同步是一种比较耗费功用的操作,应该尽量削减同步的内容,因而尽量运用同步代码块的办法来进行同步操作,同步那些需求同步的句子(这些句子一般都拜访了一些同享变量)。可是像咱们上面举得这个比方,就不得不同步办法的整个代码块,由于办法中的代码每条句子都触及了同享变量,因而此刻就能够直接运用synchronized同步办法的办法。

            (3)运用重入锁(ReentrantLock)完成线程同步

            重入性:是指同一个线程屡次企图获取它占有的锁,恳求会成功,当开释锁的时分,直到重入次数为0,锁才开释结束。

            ReentrantLock是接口Lock的一个详细完成类,和synchronized关键字具有相同的功用,并具有更高档的一些功用。如下运用:

            public class BankCount {
            private Lock lock = new ReentrantLock();
            //获取可重入锁
            private int count = 0;
            //余额
            public void addMoney(int money){
            //存钱
            lock.lock();
            try {
            count += money;
            System.out.println(System.currentTimeMillis() + "存入:" + money);
            System.out.println("账户余额:" + count);
            }
            finally{
            lock.unlock();
            }
            }
            public void getMoney(int money){
            //取钱
            lock.lock();
            try {
            if(count - money < 0){
            System.out.println("余额缺乏");
            System.out.println("账户余额:" + count);
            return;
            }
            count -= money;
            System.out.println(System.currentTimeMillis() + "取出:" + money);
            System.out.println("账户余额:" + count);
            }
            finally{
            lock.unlock();
            }
            }
            }

            部分打印成果:

            1462282419217存入:200
            账户余额:200
            1462282420217取出:200
            账户余额:0
            1462282420217存入:200
            账户余额:200
            1462282421217存入:200
            账户余额:400
            1462282421217取出:200
            账户余额:200
            1462282422217存入:200
            账户余额:400
            1462282422217取出:200
            账户余额:200
            1462282423217取出:200
            账户余额:0

            相同成果契合预期,阐明运用ReentrantLock也是能够完成同步作用的。运用ReentrantLock时,lock()和unlock()需求成对呈现,不然会呈现死锁,一般unlock都是放在finally中履行。

            synchronized和ReentrantLock的差异和运用挑选:

            1、运用synchronized取得的锁存在必定缺点

            不能中止一个正在企图取得锁的线程

            企图取得锁时不能像ReentrantLock中的trylock那样设定超时时刻 ,当一个线程取得了目标锁后,其他线程拜访这个同步办法时,有必要等候或堵塞,假如那个线程发生了死循环,目标锁就永久不会开释;

            每个锁只要单一的条件,不像condition那样能够设置多个

            2、虽然synchronized存在上述的一些缺点,在挑选上仍是以synchronized优先

            假如synchronized关键字合适程序,尽量运用它,能够削减代码犯错的几率和代码数量 ;(削减犯错几率是由于在履行完synchronized包括完的最终一句句子后,锁会主动开释,不需求像ReentrantLock相同手动写unlock办法;)

            假如特别需求Lock/Condition结构供给的独有特性时,才运用他们 ;(比方设定一个线程长时刻不能获取锁时设定超时时刻或自我中止等功用。)

            许多状况下能够运用java.util.concurrent包中的一种机制,它会为你处理一切的加锁状况;(比方当咱们在多线程环境下运用HashMap时,能够运用ConcurrentHashMap来处理多线程并发)。

            下面两种同步办法都是直接针对同享变量来设置的:

            (4)对同享变量运用volatile完成线程同步

            • volatile关键字为变量的拜访供给了一种免锁机制
            • 运用volatile润饰域适当于告知虚拟机该域或许会被其他线程更新
            • 因而每次运用该变量就要从头核算,直接从内存中获取,而不是运用寄存器中的值
            • volatile不会供给任何原子操作,它也不能用来润饰final类型的变量。

            修正BankCount类如下:

            public class BankCount {
            private volatile int count = 0;
            //余额
            public void addMoney(int money){
            //存钱
            count += money;
            System.out.println(System.currentTimeMillis() + "存入:" + money);
            System.out.println("账户余额:" + count);
            }
            public void getMoney(int money){
            //取钱
            if(count - money < 0){
            System.out.println("余额缺乏");
            System.out.println("账户余额:" + count);
            return;
            }
            count -= money;
            System.out.println(System.currentTimeMillis() + "取出:" + money);
            System.out.println("账户余额:" + count);
            }
            }

            部分打印成果:

            余额缺乏
            账户余额:200
            1462286786371存入:200
            账户余额:200
            1462286787371存入:200
            账户余额:200
            1462286787371取出:200
            账户余额:200
            1462286788371取出:200
            1462286788371存入:200
            账户余额:200
            账户余额:200
            1462286789371存入:200
            账户余额:200

            能够看到,运用volitale润饰变量,并不能确保线程的同步。volitale适当于一种“轻量级的synchronized”,可是它不能代替synchronized,volitale的运用有较强的约束,它要求该变量状况真实独立于程序内其他内容时才干运用 volatile。volitle的原理是每次线程要拜访volatile润饰的变量时都是从内存中读取,而不是从缓存傍边读取,以此来确保同步(这种原理办法正如上面比方看到的相同,多线程的条件下许多状况下仍是会存在很大问题的)。因而,咱们尽量不会去运用volitale。

            (5)ThreadLocal完成同步部分变量

            运用ThreadLocal办理变量,则每一个运用该变量的线程都取得该变量的副本,副本之间彼此独立,这样每一个线程都能够随意修正自己的变量副本,而不会对其他线程发生影响。

            ThreadLocal的首要办法有

            • initialValue():回来当时线程赋予当时线程复制的部分线程变量的初始值。一般在界说ThreadLocal类的时分会重写该办法,回来初始值;
            • get():回来当时线程复制的部分线程变量的值;
            • set(T value):为当时线程复制的部分线程变量设置一个特定的值;
            • remove():移除当时线程赋予部分线程变量的值

            如下运用:

            public class BankCount {
            private static ThreadLocal count = new ThreadLocal(){
            protected Integer initialValue() {
            return 0;
            }
            ;
            }
            ;
            //余额
            public void addMoney(int money){
            //存钱
            count.set(count.get() + money);
            System.out.println(System.currentTimeMillis() + "存入:" + money);
            System.out.println("账户余额:" + count.get());
            }
            public void getMoney(int money){
            //取钱
            if(count.get() - money < 0){
            System.out.println("余额缺乏");
            System.out.println("账户余额:" + count.get());
            return;
            }
            count.set(count.get() - money);
            System.out.println(System.currentTimeMillis() + "取出:" + money);
            System.out.println("账户余额:" + count.get());
            }
            }

            部分打印成果:

            余额缺乏
            1462289139008存入:200
            账户余额:0
            账户余额:200
            余额缺乏
            账户余额:0
            1462289140008存入:200
            账户余额:400
            余额缺乏
            账户余额:0
            1462289141008存入:200
            账户余额:600
            余额缺乏
            账户余额:0

            从打印成果能够看到,测验类中的两个线程别离具有了一份count复制,即取钱线程和存钱线程都有一个count初始值为0的变量,因而能够一向存钱可是不能取钱。

            ThreadLocal运用机遇:

            由于ThreadLocal办理的部分变量关于每个线程都会发生一份独自的复制,因而ThreadLocal合适用来办理与线程相关的相关状况,典型的办理部分变量是private static类型的,比方用户ID、事物ID,咱们的服务器运用结构关于每一个恳求都是用一个独自的线程中处理,所以事物ID对每一个线程是仅有的,此刻用ThreadLocal来办理这个事物ID,就能够从每个线程中获取事物ID了。

            ThreadLocal和前面几种同步机制的比较

            • hreadLocal和其它一切的同步机制都是为了处理多线程中的对同一变量的拜访抵触,在一般的同步机制中,是经过目标加锁来完成多个线程对同一变量的安全拜访的。这时该变量是多个线程同享的,运用这种同步机制需求很详尽地剖析在什么时分对变量进行读写,什么时分需求确定某个目标,什么时分开释该目标的锁等等许多。一切这些都是由于多个线程同享了资源形成的。
            • ThreadLocal就从另一个视点来处理多线程的并发拜访,ThreadLocal会为每一个线程保护一个和该线程绑定的变量的副本,然后阻隔了多个线程的数据,每一个线程都具有自己的变量副本,然后也就没有必要对该变量进行同步了。ThreadLocal供给了线程安全的同享目标,在编写多线程代码时,能够把不安全的整个变量封装进ThreadLocal,或许把该目标的特定于线程的状况封装进ThreadLocal。
            • ThreadLocal并不能代替同步机制,两者面向的问题范畴不同。同步机制是为了同步多个线程对相同资源的并发拜访,是为了多个线程之间进行通讯的有用办法;而ThreadLocal是阻隔多个线程的数据同享,从根本上就不在多个线程之间同享资源(变量),这样当然不需求对多个线程进行同步了。所以,假如你需求进行多个线程之间进行通讯,则运用同步机制;假如需求阻隔多个线程之间的同享抵触,能够运用ThreadLocal,这将极大地简化你的程序,使程序愈加易读、简练。

            4、锁的等级:办法锁、目标锁、类锁

            Java中每个目标实例都能够作为一个完成同步的锁,也即目标锁(或内置锁),当运用synchronized润饰一般办法时,也叫办法锁(关于办法锁这个概念我觉得仅仅一种叫法,由于此刻用来锁住办法的或许是目标锁也或许是类锁),当咱们用synchronized润饰static办法时,此刻的锁是类锁。

            目标锁的完成办法

            • 用synchronized润饰一般办法(非static);
            • 用synchronized(this){...}的办法包括代码块;

            上面两种办法取得的锁是同一个锁目标,即当时的实例目标锁。(当然,也能够运用其他传过来的实例目标作为锁目标),如下实例:

            public class BankCount {
            public synchronized void addMoney(int money){
            //存钱
            synchronized(this){
            //同步代码块
            int i = 5;
            while(i-- > 0){
            System.out.println(Thread.currentThread().getName() + ">存入:" + money);
            try {
            Thread.sleep(500);
            }
            catch (InterruptedException e) {
            e.printStackTrace();
            }
            }
            }
            }
            public synchronized void getMoney(int money){
            //取钱
            int i = 5;
            while(i-- > 0){
            System.out.println(Thread.currentThread().getName() + ">取钱:" + money);
            try {
            Thread.sleep(500);
            }
            catch (InterruptedException e) {
            e.printStackTrace();
            }
            }
            }
            }

            测验类:

            public class BankTest {
            public static void main(String[] args) {
            final BankCount bankCount = new BankCount();
            new Thread(new Runnable() {
            //取钱线程
            @Override
            public void run() {
            bankCount.getMoney(200);
            }
            }
            ,"取钱线程").start();
            new Thread(new Runnable() {
            //存钱线程
            @Override
            public void run() {
            bankCount.addMoney(200);
            }
            }
            ,"存钱线程").start();
            }
            }

            打印成果如下:

            取钱线程>取钱:200
            取钱线程>取钱:200
            取钱线程>取钱:200
            取钱线程>取钱:200
            取钱线程>取钱:200
            存钱线程>存入:200
            存钱线程>存入:200
            存钱线程>存入:200
            存钱线程>存入:200
            存钱线程>存入:200

            打印成果表明,synchronized润饰的一般办法和代码块取得的是同一把锁,才会使得一个线程履行一个线程等候的履行成果。

            类锁的完成办法:

            • 运用synchronized润饰static办法
            • 运用synchronized(类名.class){...}的办法包括代码块

            由于static的办法是归于类的,因而synchronized润饰的static办法获取到的肯定是类锁,一个类能够有许多目标,可是这个类只会有一个.class的二进制文件,因而这两种办法取得的也是同一品种锁。

            如下修正一下上面代码的两个办法:

            public void addMoney(int money){
            //存钱
            synchronized(BankCount.class){
            int i阿里后端面试题篇五——Java程序员必备:多线程、锁 = 5;
            while(i-- > 0){
            System.out.println(Thread.currentThread().getName() + ">存入:" + money);
            try {
            Thread.sleep(500);
            }
            catch (InterruptedException e) {
            e.printStackTrace();
            }
            }
            }
            }
            public static synchronized void getMoney(int money){
            //取钱
            int i = 5;
            while(i-- > 0){
            System.out.println(Thread.currentThread().getName() + ">取钱:" + money);
            try {
            Thread.sleep(500);
            }
            catch (InterruptedException e) {
            e.printStackTrace();
            }
            }
            }

            打印成果和上面相同。阐明这两种办法取得的锁是同一品种锁。

            类锁和目标锁是两种不同的锁目标,假如将addMoney办法改为一般的目标锁办法,持续测验,能够看到打印成果是替换进行的。

            • 一个线程取得了目标锁或许类锁,其他线程仍是能够拜访其他非同步办法,取得了锁仅仅阻挠了其他线程拜访运用相同锁的办法、代码块;
            • 一个取得了目标锁的线程,能够在该同步办法中持续去拜访其他相同锁目标的同步办法,而不需求从头请求锁。

            后边一篇,将总结线程池ThreadPool、生产者顾客问题及完成、sleep和wait办法差异。

            Java架构师丨苏先生:专心于Java开发技能的研讨与常识共享!

            ————END————

            • 点赞(修改不易,感谢您的支撑)
            • ...
            • 转发(共享常识,传达高兴)
            • ...
            • 重视(每天花十分钟,用实力提高自己!)
            • ...

            引荐阅览

            看到网上疯传的《阿里Java架构师生长之路》,网友瞬间欢腾了

            字节跳动Java研制面试99题(含答案):JVM+Spring+MySQL+线程池+锁

            Java程序员备战“金九银十”必备的面试技巧(附携程Java岗面试题)

            Java程序员奋斗史,为了年薪40W,知道我这五年是怎样过来的吗?

            请关注微信公众号
            微信二维码
            不容错过
            Powered By Z-BlogPHP