Detailed and easy-to-understand threading primer 2

This article has participated in the "Newcomer Creation Ceremony" event to start the road of gold creation together.

  In the previous section, we have introduced how to create threads by inheriting the methods of the Thread class. However, Java has the limitation of single inheritance. Once the Thread class is inherited, other classes cannot be inherited, so there is an implicit limitation. So in general, it is more recommended to use the method of implementing the interface to create multi-threading.
  The Thread class itself inherits the Runnable interface, and the run method we need to override comes from the Runnable interface. At the same time, the Thread class also provides a construction method, the parameter it needs is a class that implements the Runnable interface. From the above conditions, the new method of creating threads is obvious. We make a slight modification to the demo at the end of the previous section:

//除了将继承改为实现接口,这部分代码没有任何区别
class threadTest implements Runnable{
    String name;
    threadTest(String name){
        this.name=name;
    }
    @Override
    public void run() {
        for(int i=0;i<500;++i){
        }
        System.out.println(name+" finished!");
    }
}

public class LearnThread {
    public static void main(String[] args) {
        threadTest t1=new threadTest("线程1");
        threadTest t2=new threadTest("线程2");
        threadTest t3=new threadTest("线程3");
        //就像开头提到的,传入一个实现了Runnable接口的类,再调用Thread中的start方法
        new Thread(t1).start();
        new Thread(t2).start();
        new Thread(t3).start();
    }
}
复制代码

  You can compare this to the code at the end of the previous section, and I'm sure you can get a good idea of ​​what these two methods of creating threads have in common.
  Now consider a question. We have a class A that implements the Runnable interface. It can be passed into the Thread class to start a new thread. Can we also pass A into multiple Thread classes to open several threads? The answer is yes. The constructor of Thread allows you to add a string after passing in a class that implements the Runnable interface to represent the name of the thread. There is no restriction on whether the passed in class (such as A) is being used by other threads. . The above code is slightly modified as follows:

class threadTest implements Runnable{
    private static int i=50;
    @Override
    public void run() {
        while(i>0){
        //这里的Thread.currentThread.getName方法可以拿到正在执行的线程的名字
            System.out.println(Thread.currentThread().getName()+" 拿到了 "+i);
            i--;
        }

    }
}

public class LearnThread {
    public static void main(String[] args) {
        threadTest t1=new threadTest();
        new Thread(t1,"线程1").start();//我们为t1同时开启了三个线程!
        new Thread(t1,"线程2").start();
        new Thread(t1,"线程3").start();
    }
}
复制代码

  Take a look at my execution results (partial), you will find some unexpected situations

线程1 拿到了 50
线程3 拿到了 50
线程3 拿到了 48
线程3 拿到了 47
线程3 拿到了 46
线程3 拿到了 45
线程3 拿到了 44
线程3 拿到了 43
线程2 拿到了 50
线程3 拿到了 42
线程1 拿到了 49
复制代码

  你可以发现,50被三个线程分别都拿到了一次,但是在我们的run方法中,每次i被拿到都应该要减1,这实在是一种奇怪的情况。这实际上被叫做并发问题,下面我会关于它做详细的解释。
  当多个线程访问同一个对象,并且其中某些线程试图去修改该对象时,就需要实现线程同步。线程同步的条件是队列+锁。这两个概念实际上在现实生活中也被广泛地运用着。设想一下你在自主取款机取款的场景,人们在一台取款机前排队,你加入到队伍,当队伍轮到你时,你可以进入到小房间里,把门锁上,然后安全地进行你的取款过程。当你取完款后,把门打开,这样后一个人就可以进来处理他的业务。
  计算机中的线程不懂所谓的礼节,线程们不会自发地排队。但是我们可以把它们都希望得到的共享对象加上锁,只有当前一个线程结束对该对象的使用后(其余所有线程都在阻塞队列中等待着),才可以有另一个线程获得操纵该共享对象的资格(究竟是哪个线程胜出呢?这还涉及到线程间的竞争问题)。如果没有锁(取款机小房间的门)的存在,那么排着队的线程们会试图一窝蜂地一起挤到小房间里去,就像上面的demo中表现的那样。三个线程几乎同时挤进了小房间,而且都拿到了50这个数字(因为还没有任何一个线程来得及把i修改为49)。
  接下来我会介绍一种java中常见加锁的方法,即使用synchronized关键字,它有两种使用方法。
  1.在某个方法的修饰符上加synchronized,这样相当于给这个方法所在的对象,即this加锁。 来看看这个demo

public class LearnThread implements Runnable {
    private static int count=0;
    @Override
    public synchronized void run() {
            for (int i = 0; i < 5; i++) {
                    System.out.println(Thread.currentThread().getName() + " get " + (count++));
            }
    }

    public static void main(String[] args) {
        LearnThread t1 = new LearnThread();
        new Thread(t1, "线程1").start();
        new Thread(t1, "线程2").start();
    }
}
复制代码

  我们在LearnThread类的run方法上加了synchronized关键字,现在这个类被锁住了(有了一个小房间把它关在里面)。我们的三个线程只能老老实实地一个一个进入房间来操作这个加了锁的对象。下面是程序输出:

线程1 get 0
线程1 get 1
线程1 get 2
线程1 get 3
线程1 get 4
线程2 get 5
线程2 get 6
线程2 get 7
线程2 get 8
线程2 get 9
复制代码

  线程1率先拿到了锁,等它执行完自己的任务后,锁才被交给了线程2。如果没有synchronized关键字,那么结果将是混乱的,你可以试试看以加深印象。

  2.用schronized修饰一个代码块,其他线程只有在访问被修饰的代码块时才会被阻塞,你可以运行这段代码来看到这样修饰的不同之处

public class LearnThread implements Runnable {
    private static int count=0;
    private static int count2=0;
    @Override
    public  void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " 不阻塞的 " + (count2++));
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        synchronized (this) {//小括号里一般放this或者你要锁的类的名称
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " 阻塞的 " + (count++));
            }
        }
    }

    public static void main(String[] args) {
        LearnThread t1 = new LearnThread();
        new Thread(t1, "线程1").start();
        new Thread(t1, "线程2").start();
    }
}
复制代码

我的运行结果如下

线程2 不阻塞的 1
线程1 不阻塞的 0
线程1 不阻塞的 2
线程2 不阻塞的 3
线程1 不阻塞的 4
线程2 不阻塞的 5
线程1 不阻塞的 6
线程2 不阻塞的 7
线程2 不阻塞的 8
线程1 不阻塞的 9
线程2 阻塞的 0
线程2 阻塞的 1
线程2 阻塞的 2
线程2 阻塞的 3
线程2 阻塞的 4
线程1 阻塞的 5
线程1 阻塞的 6
线程1 阻塞的 7
线程1 阻塞的 8
线程1 阻塞的 9
复制代码

  可以看到,没有被syncronized修饰的代码块的访问是混乱的,因为两个线程可以同时访问到这个部分。而被组塞的部分,线程必须一个接一个地完成。

Guess you like

Origin juejin.im/post/7083371948318654494