multithreading多线程入门


备注

多线程是一种编程技术,它包括将任务划分为单独的执行线程。这些线程通过分配给不同的处理核心或通过时间切片同时运行。

在设计多线程程序时,应尽可能使线程彼此独立,以实现最大的加速。
实际上,线程很少完全独立,这使得同步变得必要。
可以使用Amdahl定律计算最大理论加速。

好处

  • 通过有效使用可用的处理资源加快执行时间
  • 允许进程保持响应,而无需拆分冗长的计算或昂贵的I / O操作
  • 轻松优先于某些操作优先于其他操作

缺点

  • 如果不仔细设计,可能会引入难以发现的错误
  • 创建线程涉及一些开销

同一个线程可以运行两次吗?

最常见的问题是,同一个线程可以运行两次。

答案就是知道一个线程只能运行一次。

如果你尝试两次运行相同的线程,它将首次执行,但第二次会出错,错误将是IllegalThreadStateException。

例子

public class TestThreadTwice1 extends Thread{  
 public void run(){  
   System.out.println("running...");  
 }  
 public static void main(String args[]){  
  TestThreadTwice1 t1=new TestThreadTwice1();  
  t1.start();  
  t1.start();  
 }  
}  
 

输出

running
       Exception in thread "main" java.lang.IllegalThreadStateException
 

死锁

当两个或多个线程的某个组的每个成员必须等待其中一个成员执行某些操作(例如,释放锁定)才能继续之前发生死锁。没有干预,线程将永远等待。

容易出现死锁设计的伪代码示例如下:

thread_1 {
    acquire(A)
    ...
    acquire(B)
    ...
    release(A, B)
}

thread_2 {
    acquire(B)
    ...
    acquire(A)
    ...
    release(A, B)
}
 

thread_1 获得A 但尚未B ,可能会发生死锁,并且thread_2 已获取B ,但不是A 如下图所示,两个线程将永远等待。 死锁图

如何避免死锁

作为一般经验法则,最小化锁的使用,并最小化锁和解锁之间的代码。

以相同顺序获取锁

thread_2 的重新设计解决了这个问题:

thread_2 {
    acquire(A)
    ...
    acquire(B)
    ...
    release(A, B)
}
 

两个线程以相同的顺序获取资源,从而避免死锁。

此解决方案称为“资源层次结构解决方案”。 Dijkstra提议将其作为“餐饮哲学家问题”的解决方案。

有时即使您指定了锁定获取的严格顺序,也可以在运行时使这种静态锁定获取顺序动态化。

考虑以下代码:

void doCriticalTask(Object A, Object B){
     acquire(A){
        acquire(B){
            
        }
    }
}
 

这里即使锁获取顺序看起来是安全的,当thread_1访问此方法时也会导致死锁,例如,Object_1作为参数A,Object_2作为参数B,thread_2按相反的顺序执行,即Object_2作为参数A,Object_1作为参数B.

在这种情况下,最好使用Object_1和Object_2通过某种计算得到一些唯一条件,例如使用两个对象的哈希码,因此每当不同的线程以任何参数顺序进入该方法时,每次该唯一条件将导出锁定获取订单。

例如,Say Object有一些唯一的密钥,例如Account对象的accountNumber。

void doCriticalTask(Object A, Object B){
    int uniqueA = A.getAccntNumber();
    int uniqueB = B.getAccntNumber();
    if(uniqueA > uniqueB){
         acquire(B){
            acquire(A){
                
            }
        }
    }else {
         acquire(A){
            acquire(B){
                
            }
        }
    }
}
 

Hello Multithreading - 创建新线程

这个简单的例子展示了如何在Java中启动多个线程。请注意,不保证线程按顺序执行,并且执行顺序可能因每次运行而异。

public class HelloMultithreading {

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(new MyRunnable(i));
            t.start();
        }
    }

    public static class MyRunnable implements Runnable {

        private int mThreadId;

        public MyRunnable(int pThreadId) {
            super();
            mThreadId = pThreadId;
        }

        @Override
        public void run() {
            System.out.println("Hello multithreading: thread " + mThreadId);
        }

    }

}
 

目的

线程是发生命令处理的计算系统的低级部分。它由CPU / MCU硬件支持/提供。还有软件方法。如果可能的话,多线程的目的是相互并行进行计算。因此,可以在较小的时间片中获得期望的结果。

比赛条件

数据争用或竞争条件是多线程程序未正确同步时可能发生的问题。如果两个或多个线程在没有同步的情况下访问同一个内存,并且至少有一个访问是“写入”操作,则会发生数据争用。这导致程序的平台依赖性,可能不一致的行为。例如,计算结果可能取决于线程调度。

读者 - 作家问题

writer_thread {
    write_to(buffer)
}

reader_thread {
    read_from(buffer)
}
 

简单的解决方案:

writer_thread {
    lock(buffer)
    write_to(buffer)
    unlock(buffer)
}

reader_thread {
    lock(buffer)
    read_from(buffer)
    unlock(buffer)
}
 

如果只有一个读取器线程,这个简单的解决方案很有效,但如果有多个读取器线程,则会不必要地减慢执行速度,因为读取器线程可以同时读取。

避免此问题的解决方案可能是:

writer_thread {
    lock(reader_count)
    if(reader_count == 0) {
        write_to(buffer)
    }
    unlock(reader_count)
}

reader_thread {
    lock(reader_count)
    reader_count = reader_count + 1
    unlock(reader_count)

    read_from(buffer)

    lock(reader_count)
    reader_count = reader_count - 1
    unlock(reader_count)
}
 

请注意, reader_count 在整个写入操作中reader_count 被锁定,因此在写入尚未完成时,没有读者可以开始阅读。

现在许多读者可以同时阅读,但可能会出现一个新问题: reader_count 可能永远不会达到0 ,这样编写器线程永远不能写入缓冲区。这被称为饥饿 ,有不同的解决方案来避免它。


即使是看似正确的程序也可能存在问题:

boolean_variable = false 

writer_thread {
    boolean_variable = true
}

reader_thread {
    while_not(boolean_variable)
    {
       do_something()
    }         
}
 

示例程序可能永远不会终止,因为读者线程可能永远不会看到来自编写器线程的更新。例如,如果硬件使用CPU缓存,则可以缓存这些值。并且由于对正常字段的写入或读取不会导致刷新缓存,因此读取线程可能永远不会看到更改的值。

C ++和Java在所谓的内存模型中定义了正确同步的含义: C ++ Memory ModelJava Memory Model

在Java中,解决方案是将字段声明为volatile:

volatile boolean boolean_field;
 

在C ++中,解决方案是将字段声明为原子:

std::atomic<bool> data_ready(false)
 

数据竞争是一种竞争条件。但并非所有竞争条件都是数据竞赛。由多个线程调用的以下内容会导致竞争条件,但不会导致数据争用:

class Counter {
    private volatile int count = 0;

    public void addOne() {
     i++;
    }
}
 

它根据Java内存模型规范正确同步,因此它不是数据竞争。但它仍会导致竞争条件,例如结果取决于线程的交错。

并非所有数据竞争都是错误。所谓的良性竞争条件的一个例子是sun.reflect.NativeMethodAccessorImpl:

class  NativeMethodAccessorImpl extends MethodAccessorImpl {
    private Method method;
    private DelegatingMethodAccessorImpl parent;
    private int numInvocations;
    
    NativeMethodAccessorImpl(Method method) {
        this.method = method;
    }

    public Object invoke(Object obj, Object[] args)
        throws IllegalArgumentException, InvocationTargetException
    {
        if (++numInvocations > ReflectionFactory.inflationThreshold()) {
              MethodAccessorImpl acc = (MethodAccessorImpl)
            new MethodAccessorGenerator().
            generateMethod(method.getDeclaringClass(),
                             method.getName(),
                             method.getParameterTypes(),
                             method.getReturnType(),
                             method.getExceptionTypes(),
                             method.getModifiers());
                             parent.setDelegate(acc);
          }
          return invoke0(method, obj, args);
    }
    ...
}
 

这里代码的性能比numInvocation的计数的正确性更重要。