【Java】多线程与并发学习笔记

实现多线程的方法

查看Thread类的源码,可以发现它实现了Runnable接口,然后在自己的run方法中调用了Runnable的run方法。这里其实就是静态代理这一设计模式,Thread是代理角色,而Runnable则是真实角色。在Thread的start方法中,会为我们开辟新的线程并执行其run方法,而Thread的run方法会调用它内部持有引用的Runnable的run方法。因此,我们有以下几种方法开辟新线程:

1. 重写Thread的run方法

通过重写Thread的run方法,不再传给它Runnable,通过其Runnable的run方法执行,而是在的run方法中直接执行相应的操作。

Thread thread = new Thread(){
    @Override
    public void run() {
        //TODO
    }
};
thread.start();

2. 通过Runnable的run方法

通过创建一个Runnable,通过静态代理的设计,将Runnable对象传给Thread,再通过Thread开辟新线程并做相应操作

Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        //TODO
    }
});
thread.start();

3. 通过Callable接口

通过Callable<T>接口实现多线程,比较繁琐,优点是有返回值。

创建一个类实现Callable<T>接口,其中泛型为返回的值的类型、然后实现Callable的call方法。

class Race implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        return 1000;
    }
}

借助执行调度工具ExecutorService,创建对应类型线程池。获取Future对象(submit方法中的参数是具体实现类对象)。然后通过Future的get方法即可获取到返回值(此处要做异常处理,将异常throws了)。

get这里会等待线程中步骤执行完然后获取数据。

ExecutorService service = Executors.newFixedThreadPool(2);
Race race = new Race();
Future<Integer> result = service.submit(race);
int num = result.get();

调用ExecutorService的shutdown方法停止服务

service.shutdown();

线程的状态

线程有五大状态,分别是

  • new:新建状态
  • Runnable:就绪状态
  • Running:运行状态
  • Blocked:阻塞状态
  • Dead:死亡状态

img

上面的图是线程的状态的示意图。

当我们创建线程后,它便进入了new(新建)状态。

在我们调用了它的start方法后,它便进入了Runnable(就绪)状态。

经过CPU的调度,它便会进入Running(运行)状态。

如果在运行后执行完毕,这个线程便会进入Dead(死亡)状态。

如果在运行过程中一些事情导致了线程的阻塞,它就会进入Blocked(阻塞)状态。阻塞解除后它便会重新进入Runnable(就绪状态)

线程的停止

1. 自然终止

当线程体自然执行完毕后,线程便会自然终止。

2. 外部干涉

外部干涉非常简单,比如下面这种方法

线程类中定义线程体使用标志

class Study implements Runnable{
    private boolean flag = true;
    @Override
    public void run() {
        //TODO
    }
}

线程体内使用flag

class Study implements Runnable{
    private boolean flag = true;
    @Override
    public void run() {
        while (flag){
            System.out.println("study thread");
        }
    }
}

提供对外方法改变该标识

class Study implements Runnable{
    private boolean flag = true;
    ...
    public void stop(){
        this.flag = false;
    }
}

外部调用

Study study = new Study();
new Thread(study).start();
for (int i=0;i<100;i++){
    if (i==50)
        study.stop();
}

3. Thread的stop方法

这个方法已经被弃用,并且臭名昭著。从他的字面意思上我们可以知道他貌似可以停止一个线程。但实际上它并非是真正意义上的停止线程,而且停止线程还会引来一些其他的麻烦事。

调用Thread.stop()方法是不安全的,这是因为当调用Thread.stop()方法时,会发生下面两件事:

  1. 即刻抛出ThreadDeath异常,在线程的run()方法内,任何一点都有可能抛出ThreadDeath异常,包括在catch或finally语句中。
  2. 会释放该线程所持有的所有的锁,而这种释放是不可控制的,非预期的。

当线程抛出ThreadDeath异常时,会导致该线程的run()方法突然返回来达到停止该线程的目的。ThreadDetath异常可以在该线程run()方法的任意一个执行点抛出

但是,线程的stop()方法一经调用线程的run()方法就会即刻返回吗?

我们用下面的例子来测试一下

public class ThreadStopTest {  
    public static void main(String[] args) {    
        try {    
            Thread t = new Thread() {    
                //对于方法进行了同步操作,锁对象就是线程本身  
                public synchronized void run() {    
                    try {    
                        long start=System.currentTimeMillis();    
                        //开始计数  
                        for (int i = 0; i < 100000; i++)    
                            System.out.println("runing.." + i);    
                        System.out.println((System.currentTimeMillis()-start)+"ms");    
                    } catch (Throwable ex) {    
                        System.out.println("Caught in run: " + ex);    
                        ex.printStackTrace();    
                    }    
                }    
            };    
            //开始计数  
            t.start();  
            //主线程休眠100ms  
            Thread.sleep(100);    
            //停止线程的运行  
            t.stop();  
        } catch (Throwable t) {    
            System.out.println("Caught in main: " + t);    
            t.printStackTrace();    
        }    

    }    
}  

会发现,程序运行的结果如图:

...

running..99994

running..99995

running..99996

running..99997

running..99998

running..99999

可以看到,调用了stop方法之后,线程并没有停止,而是将run方法执行完 。这就十分诡异了。根据SUN的文档,原则上只要一调用thread.stop()方法,那么线程就会立即停止,并抛出ThreadDeath异常。

查看Thread的源代码后发现 ,Thread.stop(Throwable obj)方法是同步的 ,而我们工作线程的run()方法也是同步。会导致主线程和工作线程共同争用同一个锁(工作线程对象本身) 。 由于工作线程在启动后就先获得了锁,所以无论如何,当主线程在调用stop()时,它必须要等到工作线程的run()方法执行结束后才能进行,结果导致了上述奇怪的现象。

因此,不要用stop方法停止线程

线程的阻塞

1. join 合并线程

join可以理解为合并的意思,将多条线程合并为一条。下面是join的几个重载

  • join():等待该线程终止
  • join(long millis):等待该线程终止的时间最长为millis毫秒
  • join(long millis, int nanos):等待该线程终止的时间最长为millis毫秒+nanos纳秒

我们可以通过下面的例子来看看join的效果

class Thread1 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100 ; i++) {
            System.out.println("join..."+i);
        }
    }

    public static void main(String[] args){
        Thread1 thread1 = new Thread1();
        Thread t = new Thread(thread1); //新生
        t.start();  //就绪
        //cpu调度后则进入运行状态
        for (int i = 0; i < 100 ; i++) {
            if (i==50){
                try {
                    t.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("main..."+i);
        }
    }
}

查看运行结果:

join...0
main...0
join...1
main...1
join...2
main...2
...
main...48
join...49
main...49
join...50
join...51
join...52
...
join...97
join...98
join...99
main...50
main...51
main...52
...

查看运行结果,会发现。在执行join之前,main线程与 t 线程是多线程同时执行的。而当执行了t的join方法之后,等到 t 线程执行完后,main线程才继续执行。也就是在 t 线程执行的过程中,main线程被阻塞了。等到 t 线程执行完,main线程才继续执行。

2. yield 暂停线程

yield方法可以实现暂停当前线程,执行其他的线程。

它是一个static方法,暂停的具体线程取决于它执行的地方。

3. sleep 休眠线程

sleep方法可以让当前正在执行的线程进行休眠一定秒数。

  • sleep(long millis):让当前线程休眠millis毫秒
  • join(long millis, int nanos):让当前线程休眠millis毫秒+nanos纳秒

每一个对象都有一把锁。当该线程休眠的时候,不会释放锁。

sleep可以有以下的作用:

1. 与时间相关:倒计时
2. 模拟网络延时

线程的优先级

我们可以为线程通过setPriority方法设置优先级,优先级的范围为1-10。下面是几个优先级的常量:

  • MIN_PRIORITY:最小优先级,值为1
  • NORM_PRIORITY:正常优先级,线程的默认优先级,值为5
  • MAX_PRIORITY:最大优先级,值为10

注意,优先级并不能代表先后顺序,只能导致执行的概率不同。优先级大的线程执行的概率会更大。

线程的同步

同步也称为并发。由于现在有多个线程,就可能导致多个线程访问同一份资源的问题。我们要确保这个资源安全,因此我们对其进行同步的处理。这样就可以保证它线程安全

也就是说,线程安全的资源就是指多个线程同时访问这个资源的时候,是同步的。这份资源是安全的。

概念介绍

原子性

原子是世界上的最小单位,具有不可分割性。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作是原子操作。 比如:a++; 这个操作实际是a = a + 1;是可分割的 ,因此它不是原子操作。

非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。

**在 Java 中 synchronized 和在 lock、unlock 中操作保证原子性。 **

可见性

可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。 也就是一个线程修改的结果。另一个线程马上就能看到。

用volatile修饰的变量,就会具有可见性。volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。

需要注意的是,**volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。 **

**在 Java 中 volatile、synchronized 和 final 实现可见性。 **

有序性

Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 是因为其本身包含“禁止指令重排序”的语义,synchronized 是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。

Java内存模型

Java虚拟机有自己的内存模型(Java Memory Model,JMM),JMM可以屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的内存访问效果。

JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。

img

happens-before原则

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

指令重排序

介绍

Java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。

意义

JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。

synchronized关键字

我们通过synchronized标识符可以保证某一部分的线程安全,它是通过等待来实现的。我们把使用synchronized的行为叫做‘加锁’。当几个线程同时访问一份资源的时候,先到达的线程拿到锁,然后再轮到其他线程来。这样能保证线程的安全。

当我们加上synchronized后,就是为它加了一把锁。当多个线程同时来访问这份资源时,先到达的资源便可以得到这个锁,其他资源只能等待,等待结束后再执行。这样就保证了我们的线程安全。

由于线程安全是通过等待来进行,因此会造成效率的低下。为了减少这种效率损耗,我们应该尽量缩小加锁的范围,来提高效率。

同步块

我们都知道,{}及里面的语句表示一个同步块。在它前面加上synchronized(引用类型)标识符,就变成了一个同步块。

而synchronized锁 类.class 时,比较特殊。主要是用于给一些静态的对象加锁。(如线程安全的单例模式就会用到)

synchronized(引用类型|this|类.class){
    //TODO
}

同步方法

在方法的前面加上synchronized,就称这个方法是同步方法。0

volatile关键字

线程对于普通的共享对象的操作发生在本地内存中,有时可能来不及同步到主内存中,就开始了下一个线程的处理。因此我们就可以用volatile来保证这个共享对象的可见性,强制将内存刷新到主内存中。

volatile具备两种特性,第一就是保证共享变量对所有线程的可见性。将一个共享变量声明为volatile后,会有以下效应:

    1.当写一个volatile变量时,JMM会把该线程对应的本地内存中的变量强制刷新到主内存中去;

    2.这个写会操作会导致其他线程中的缓存无效。

死锁

过多的同步容易造成死锁。

对一个对象多线程同步访问,容易造成死锁。我们可以通过信号量来解决这个问题。

信号量

介绍

之前讲到线程锁,一个线程如果锁定了一资源,那么其它线程只能等待资源的释放。也就是一次只有一个线程执行,直到这个线程执行完毕或者unlock。而信号量(Semaphore)可以控制多个线程同时对某个资源的访问。

信号量(Semaphore)是用来保护一个或者多个共享资源的访问,Semaphore内部维护了一个计数器,其值为可以访问的共享资源的个数。一个线程要访问共享资源,先获得信号量,如果信号量的计数器值大于1,意味着有共享资源可以访问,则使其计数器值减去1,再访问共享资源。

就好比一个厕所管理员,站在门口。如果有厕所有空位,就开门允许与空厕数量等量的人进入厕所。多个人进入厕所后,相当于N个人来分配使用N个空位。为了避免多个人来同时竞争同一个厕所,在内部仍然使用锁来控制资源的同步访问。

使用

Semaphore使用时需要先构建一个参数来指定共享资源的数量,Semaphore构造完成后即可获取Semaphore。共享资源使用完毕后释放Semaphore。

Semaphore semaphore = new Semaphore(10,true);
semaphore.acquire();
//TODO
semaphore.release();

比如下面的代码就是模拟控制一个商场厕所的并发使用:

public class ResourceManage {  
    private final Semaphore semaphore ;  
    private boolean resourceArray[];  
    private final ReentrantLock lock;  
    public ResourceManage() {  
        this.resourceArray = new boolean[10];//存放厕所状态  
        this.semaphore = new Semaphore(10,true);//控制10个共享资源的使用,使用先进先出的公平模式进行共享(公平模式的信号量,先来的先获得信号量)  
        this.lock = new ReentrantLock(true);//公平模式的锁,先来的先选  
        for(int i=0;i<10; i++){  
            resourceArray[i] = true;//初始化为资源可用的情况  
        }  
    }  
    public void useResource(int userId){ 
        semaphore.acquire(); 
        try{  
            int id = getResourceId();//占到一个坑  
            System.out.print("userId:"+userId+"正在使用资源,资源id:"+id+"\n");  
            Thread.sleep(100);//do something,相当于于使用资源  
            resourceArray[id] = true;//退出这个坑  
        }catch (InterruptedException e){  
            e.printStackTrace();  
        }finally {  
            semaphore.release();//释放信号量,计数器加1  
        }  
    }  
    private int getResourceId(){  
        int id = -1; 
        lock.lock();
        try {  
            //虽然使用了锁控制同步,但由于只是简单的一个数组遍历,效率还是很高的,所以基本不影响性能。  
            for(int i=0; i<10; i++){  
                if(resourceArray[i]){  
                    resourceArray[i] = false;  
                    id = i;  
                    break;  
                }  
            }  
        }catch (Exception e){  
            e.printStackTrace();  
        }finally {  
            lock.unlock();  
        }  
        return id;  
    }  
}  
public class ResourceUser implements Runnable{  
    private ResourceManage resourceManage;  
    private int userId;  
    public ResourceUser(ResourceManage resourceManage, int userId) {  
        this.resourceManage = resourceManage;  
        this.userId = userId;  
    }  
    public void run(){  
        System.out.print("userId:"+userId+"准备使用资源...\n");  
        resourceManage.useResource(userId);  
        System.out.print("userId:"+userId+"使用资源完毕...\n");  
    }  

    public static void main(String[] args){  
        ResourceManage resourceManage = new ResourceManage();  
        Thread[] threads = new Thread[100];  
        for (int i = 0; i < 100; i++) {  
            Thread thread = new Thread(new ResourceUser(resourceManage,i));//创建多个资源使用者  
            threads[i] = thread;  
        }  
        for(int i = 0; i < 100; i++){  
            Thread thread = threads[i];  
            try {  
                thread.start();//启动线程  
            }catch (Exception e){  
                e.printStackTrace();  
            }  
        }  
    }  

点赞

发表评论

电子邮件地址不会被公开。必填项已用 * 标注

%d 博主赞过: