Home  >  Article  >  Java  >  Java concurrency--detailed explanation of all thread mechanism examples

Java concurrency--detailed explanation of all thread mechanism examples

Y2J
Y2JOriginal
2017-04-25 09:20:461428browse

JAVA multi-threading is not a simple knowledge point, but consists of many trivial contents put together. There are many mechanisms that we can't name but they are very important. Here we will go through all the commonly used concurrency mechanisms.

Sleep and concession


A simple way to objectively affect thread tasks is to call the sleep method. The sleep method suspends execution to After this period of time, continue the operation in the program. Different from this, we use the yield method after the run method completes a cycle. The yield method indicates to the CPU that the work of this thread is almost done, and other threads (with the same priority) can use the CPU. .

sleep

The sleep method will cause your thread to sleep for a fixed period of time, and then wake up to continue executing the code. This allows the thread scheduler to switch to another thread, which in turn drives another task. However, which task is driven depends on the underlying thread mechanism and operating system. We cannot rely on this execution order for program security. Either use synchronization control, or do not use threads at all, and only use cooperative routines. These routines will transfer control to each other in the specified order.

The call to sleep may throw an InterruptedException, which will be caught in the run method. Because exceptions cannot be propagated back to the main function across threads.

yield

We use the yield method after the run method completes a cycle. The yield method indicates to the CPU that the work of this thread is almost done and can allow other ( ) threads with the same priority to use the CPU. However, it should be noted that the representation of yield to the CPU may not be 100% adopted. In fact, yield is often misused.

Thread priority


Each thread has a priority. What we need to know is that without special processing, all threads The priorities are the same.

By default, we divide the priorities into between 1 and 10, and high-priority threads will be operated first. Speaking of this, people can't help but think of terms such as process priority and aging in the operating system. In fact, the Java virtual machine does use the priority of the process to analogize the priority of the thread. But the biggest problem with this is that each operating system handles process priority differently, and Java's thread priority also has platform variability.

We can get the priority of the thread through the getPriority() method, and we can also use setPriority() to modify it at any time.

Thread.currentThread().getPriority(); 
  //获取线程优先级 
  Thread.currentThread().setPriority(); 
  //修改线程优先级

We should not depend on the thread priority for the correctness of the program, we should use the thread priority as little as possible.

Daemon thread


daemon thread, we can translate it into a daemon thread or a background thread.

The role of the daemon thread is to provide services for other threads. If all other threads are exited and only the daemon thread is left, then the program will end. There is no need to run the daemon thread separately. For example, for other threads' timers, we can set it as a daemon thread. And the child threads derived from the daemon thread are also daemon threads.

t.setDaemon(ture);  
  //将线程转换为守护线程 
  t.isDaemon();  
  //判断线程是否为守护线程

Do not open or use any resources in the daemon thread! The program terminates when all non-daemon threads exit, then you have to think that the background will terminate its run method without executing the finally clause of the daemon thread. You may not believe it, but it is true. System.exit(0); is the only situation where the finally clause is not executed.

Join thread


A thread can call the join method on other threads. The effect is to wait for a period of time until the called thread ends, and then return to this thread to continue downwards.

First, in this thread, you need to obtain a reference from another thread, and use this reference to call the join() method. After the call, this thread will be suspended until the target thread ends. You can also bring a timeout parameter when calling join, so that if the target thread has not ended when this period of time expires, the join method can also return.

In JAVA SE5, the java.util.concurrent class library has newly added tools such as CyclicBarrier, which may be more effective than join in the original thread class library.

Thread Group


Regarding thread group, it is still very popular in JDK1.2 version, but with the introduction of subsequent versions, thread group is not So easy to use, we can ignore it for thread groups.

It's best to think of thread groups as an unsuccessful attempt that you can just ignore. ——Joshua Bloch

捕获异常


因为多线程的机制,我们不能在main函数中捕获其他线程的异常。这就说明,如果我们不能在run方法中自己捕获异常,那么异常会被抛出到控制台上。除非我们采用特殊的机制来捕获。

package AllThread;/**
 * 
 * @author QuinnNorris
 * 
 *         捕获异常
 */public class ExceptionThread {

    /**
     * @param args
     */
    public static void main(String[] args) {        // TODO Auto-generated method stub

        Thread th = new Thread(new Runnable() {            @Override
            public void run() {                // TODO Auto-generated method stub
                throw new RuntimeException();
            }
        });
        th.start();
    }

}

这是一段简单的代码,它会抛出一个运行时异常:

Exception in thread “Thread-0” java.lang.RuntimeException 
          at AllThread.ExceptionThread$1.run(ExceptionThread.java:15) 
          at java.lang.Thread.run(Thread.java:745)

我们可以看出, 由于没有去设计捕获异常,它被直接输出到控制台上。对于这种情况,为main函数加上try-catch语句是没有用的。

为了解决这种不能捕获未检查异常的情况,在JAVA SE5中引入了使用Executor的一种解决方法。

package AllThread;import java.lang.Thread.UncaughtExceptionHandler;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.ThreadFactory;/**
 * 
 * @author QuinnNorris
 * 
 *         使用UncaughtExceptionHandler捕获异常
 */public class UEHThread {

    /**
     * @param args
     */
    public static void main(String[] args) {        // TODO Auto-generated method stub
        ExecutorService es = Executors.newCachedThreadPool(new ThreadFactory() {            @Override
            public Thread newThread(Runnable r) {                // TODO Auto-generated method stub
                Thread th = new Thread(r);
                th.setUncaughtExceptionHandler(new UncaughtExceptionHandler() {                    @Override
                    public void uncaughtException(Thread t, Throwable e) {                        // TODO Auto-generated method stub
                        System.out.println("catch it " + e);
                    }
                });                return th;
            }
        });
        es.execute(new Runnable() {            @Override
            public void run() {                // TODO Auto-generated method stub
                throw new RuntimeException();
            }
        });
    }
}

因为我比较懒全部用内部类来表示,所以这段程序可能略有些难懂。首先我们创建了一个线程池,然后为这个创建线程池的静态方法赋予一个参数。这个参数是一个ThreadFactory类,这个类是用来描述在线程池中的线程具有的共性的。ThreadFactory有一个方法需要我们覆盖就是newThread方法,这个方法的参数是我们要处理的Runnable任务,也就是我们要加入到线程池中的Runnable任务。我们在这个方法中用一个th对象包含r对象,然后设置th对象的UncaughtExceptionHandler属性。这个setUncaughtExceptionHandler方法的参数是一个UncaughtExceptionHandler对象(这里我们第二次用内部类),UncaughtExceptionHandler类的唯一一个方法是uncaughtException。这个方法用来表示对线程未检查异常的处理方式,我们让他在控制台输出一句话。到这里我们对线程池的部署就完成了。

然后我们在这个线程池中添加一个Runnable任务,这个任务会抛出一个未检查异常。现在我们运行程序,控制台输出:

catch it java.lang.RuntimeException

到此,对于线程run方法中的未检查异常的处理就结束了。需要注意的是,我们向线程池中添加线程的方法要调用execute方法而不要使用submit方法,submit方法会把异常吞掉。从而控制台将会什么都不输出。

竞争条件


在操作系统中有一张让人印象深刻的图片。上面画的是一块块并排的进程,在这些进程里面分了几个线程,所有这些线程齐刷刷统一的指向进程的资源。资源会在线程间共享而不是每个线程都有一份独立的资源。在这种共享的情况下,很有可能有多个线程同时在访问一个资源,这种现象我们叫做竞争条件。

在一个银行系统中,每个线程分别管理一个账户,这些线程可能会进行转账的操作。  
 在一个线程进行操作的时候,他首先,会把账户余额存放到寄存器中,第二步,它将寄存器中的数字减少要转出的钱数,第三步,它将结果写回余额中。  
 问题在于,这个线程在执行完1、2步时,另外一个线程被唤醒并且修改了第一个线程的账户余额值,但是这个时候第一个线程并不知情。第一个线程等待第二个线程执行完毕后,继续他的第三步:将结果写回余额中。这个时候,它把第二个线程的操作刷掉了,所以钱数发生错误。

同步规则


如果你正在写一个变量,他可能接下来将被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,那么你必须使用同步,并且,读写线程都必须用相同的监视器锁同步。——Brian

ReentrantLock


上面的例子告诉我们:如果我们的操作不是原子操作,被打断是肯定会发生的。我们没办法把代码变成原子操作,但是能将其上锁来保证安全性。在并发程序中,在访问资源或数据之前,要先给代码套一个锁。在锁被使用的期间,代码中涉及的资源不能被其他的线程访问,直到程序结束时再将锁打开。

ReentrantLock构造器

ReentrantLock类提供了两个构造器:一个是默认构造器,一个是带有公平策略的构造器。

首先,带有公平策略的锁会比正常的锁要慢很多。其次,在某些情况下公平策略并不能保证真正公平的。
 如果我们没有特殊的理由真的需要公平策略的时候,尽量不要去使用这种锁。

锁的获取与释放

ReentrantLock myLock = new ReentrantLock();
//创建对象
myLock.lock();
//获取锁try{...}
finally{
myLock.unlock();
//释放锁
}

一定要在finally中释放锁。如果不在finally中释放锁,锁确实将一直得不到释放。正如同我们在调用资源后会使用close()方法。值得一提的,当我们使用锁的时候,我们不能使用try-with-resource,因为这个锁并不是用close来关闭的。

ReentrantLock具有可重入性

如果你要在递归或者循环程序中使用锁,那么就放心的用吧。ReentrantLock锁具有可重入性,他会在每次调用lock()的时候维护一个计数记录着被调用的次数,在每一次的lock调用都必须要用unlock来释放。

条件对象


if(a>b) a.set(b-1);

上面是一个很简单的条件判断,但是我们在并发程序中不能直接这样书写。如果在这个线程刚刚做完判断之后,另外一个线程被唤醒,并且另外一个线程在操作之后使得a小于b(if语句中的条件已经不再正确)。但我们还会执行if中的语句,这是不正确的。

或许会想把整个if语句直接放在锁里面,确保自己的代码不会被打断。但是这样又存在一个问题,如果if判断是false,那么if中的语句不会被执行。但如果我们需要去执行if中的语句,甚至我们要一直等待if判断变的正确之后去执行if中的语句的情况下,这时if语句再也不会变得正确了,因为我们的锁把这个线程锁死,其他的线程没办法访问临界区并修改a和b的值让if判断变得正确。这时候我们只能放弃锁,等待其他线程使用,再获得锁,进行判断,如果判断仍未false就重复之前的操作。这种繁琐的过程是我们不希望的。

通常,线程在上锁进入临界区之后存在一个问题:线程所需的资源,在别的线程中使用或并不满足他们能执行的条件,这个时候我们需要用一个条件对象来管理这些得到了一个锁,但是不能做有用工作的线程

Condition


Condition类在临界区起到了条件对象的作用。

我们用ReentrantLock类中的newCondition方法来获取一个条件对象。

Condition cd = myLock.newCondition();

我们在if语句下面直接跟上await方法,这个方法表示这个线程被阻塞,并放弃了锁,进入等待状态等其他的线程来操作。其他的线程在顺利执行if语句内容之后,调用signalAll方法,这个方法将会重新去激活所有的因为这个条件被阻塞的线程,让这些线程重新获得机会,这些线程被允许从被阻塞的地方继续进行。此时,线程应该再次测试该条件,如果还是不能满足条件,需要再次重复上述操作。

ReentrantLock myLock = new ReentrantLock();//创建锁对象myLock.lock();//给下面的临界区上锁Condition cd = myLock.newCondition();//创建一个Condition对象,这个cd对象表示条件对象while(!(a>b))
    cd.await();//上面的while循环和await方法调用是标准写法//如果不能满足if的条件,那么他将进入阻塞状态,放弃锁,等待别人去激活它a.set(b-1);//一直等到从while循环出来,满足了判断的条件,我们执行自己的功能cd.signalAll();//调用signalAll方法去激活其他的被阻塞的线程。如果所有的线程都在等待其他线程signalAll,则进入死锁

总结来说,Condition对象和锁有这样几个特点。

  1. 锁可以用来保护代码片段,任何时刻只能有一个线程进入被保护的区域

  2. 锁可以管理试图进入临界区的线程

  3. 锁可以拥有一个或多个条件对象

  4. 每个条件对象管理那些因为前面所描述的原因而不能被执行但已经进入被保护代码段的线程

synchronized


ReentrantLock和Condition对象是一种用来保护代码片段的方法。还可以通过使用关键字synchronized来修饰方法,从而给方法添加一个内部锁。java的每一个对象都有一个内部锁,每个内部锁会保护那些被synchronized修饰的方法。也就是说,如果想调用这个方法,首先要获得内部的对象锁。

所有对象都自动含有单一的锁(也叫做监视器)。当在对象上调用其任意synchronized方法的时候,此对象都被加锁,这时该对象上的其他synchronized方法只有等到前一个方法调用完毕并释放了锁之后才能被调用。

与ReentrantLock比较

我们先拿出上面的代码:

public void function(){
    ReentrantLock myLock = new ReentrantLock();
    myLock.lock();

    Condition cd = myLock.newCondition();    while(!(a>b))
        cd.await();

    a.set(b-1);

    cd.signalAll();
}

如果我们用synchronized来实现这段代码,将会变成下面的样子:

public synchronized void function(){
    while(!(a>b))
        wait();

    a.set(b-1);

    notifyAll();
}

需要我们注意的是,在使用synchronized关键词时,无需再去用ReentrantLock和Condition对象,我们用wait方法替换了await方法,notifyAll方法替换了signalAll方法。这样写确实比之前的简单了很多。

静态synchronized

将静态方法声明为synchronized也是合法的。如果调用这种方法,将会获取相关的类对象的内部锁。比如我们调用Test类中的静态方法,这时,Test.class对象的锁将被锁住。

Limitations of internal locks and conditions

Although internal locks are simple, they have many limitations:

  1. Cannot interrupt an ongoing process The thread trying to acquire the lock

  2. cannot set a timeout when trying to acquire the lock

  3. because the condition cannot be instantiated through Condition. Having only a single condition per lock may not be enough

The above is the detailed content of Java concurrency--detailed explanation of all thread mechanism examples. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn