Java(并发)


目前只总结了一些基础并发的机制,有关JUC的其他组件以及锁的问题会持续更新~~

1、线程

Java线程机制

Java的线程机制是抢占式的,调度机制会周期性的中断线程,将上下文切换到另一个线程,并为其提供合理的时间片去驱动任务。
使用多线程机制,可以将多个分离、独立运行的任务交由执行线程来驱动。一个线程就是在进程中的一个单一的顺序控制流。

使用线程来驱动任务

1.实现Runnable接口,并重写run()方法。注意:此时的类只能当作一个可以被线程驱动的任务(下面的实现Callable接口也是如此),并不是真正的线程,还需要将任务提交给Thread构造器(需要一个Runnable对象)

public class MyRunnable implements Runnable {
	 @Override
	 public void run() {
	 	//通常run方法内总会有循环
	 }
}
//
public class DriveTask{
	psvm{
		Thread thread = new Thread(new MyRunnable());
		thread .start();
	}
}

t.start();//是线程执行必须调用的初始化操作,然后才会调用run()方法,而调用run()方法时,main方法的其余操作仍会执行。
当有多个线程时,线程调度机制会随机执行某一个线程,因此同一个程序的两次执行结果可能不同。线程调度机制是非确定性的

2.实现Callable接口,并重写call()方法。与实现Runnable接口不同的是,Callable可以有返回值,并通过FutureTask封装。

public class MyCallable implements Callable< String>{
	 @Override
	 public String call() throws Exception {
	 	//...
	 }
}
//
public class DriveTask{
	psvm{
		MyCallable mc = new MyCallable();
		//FutureTask包装器可以将Callable转换成Runnable和Future
		FutureTask< String> t = new FutureTask< String>(mc);
		Thread thread = new Thread(t);//Runnable
		thread.start();
		String result = t.get();//Future
		
		//当有多个任务执行时(线程池),可以使用ArrayList< Future< String>> results = ...将FutureTask隐藏
		ExecutorService exec = Executtors.newCachedThreadPool();
		ArrayList< Future< String>> results = ...
		for(...){ results.add(exec.submit(new MyCallable()))} //调用call方法
		for(Future< String> f : results){ f.get(); } //遍历结果
	}
}

下面在对Future接口做一解释

public interface Future< V> {
	//...
	V get() throws InterruptedException, ExecutionException; //得到任务执行后的结果
	V get(long timeout, TimeUnit unit)throws InterruptedException, ExecutionException, TimeoutException;
}

继承Thread类:与实现接口相比,还是实现接口会更好一些。因为:

  • Java 不⽀持多重继承,因此继承了 Thread 类就⽆法继承其它类,但是可以实现多个接⼝;
  • 类可能只要求可执⾏就⾏,继承整个 Thread 类开销过⼤。
public class MyThread extends Thread {
	@Override
	public void run() {
		// ...
	}
}
public static void main(String[] args) {
	MyThread mt = new MyThread();
	mt.start();
}

2、Executor、线程优先级、守护线程、sleep()和yield()、InterruptedException

执行器(Executor)在客户端和任务执行之间提供了一个间接层,这个中介对象将执行任务。Executor允许管理多个异步任务的执行,而无需显式管理线程的生命周期。ExecutorService 是具有服务生命周期的Executor,使用静态的Executor方法创建。

ExecutorService
        exec1 = Executors.newCachedThreadPool(), //可为所有任务创建一个线程
        exec2 = Executors.newFixedThreadPool(5), //包含有限个可以执行任务的线程
        exec3 = Executors.newSingleThreadExecutor(); //相当于数量为1的FixedThreadPool
        //当有多个任务被提交,那么这些任务将排队直到轮到自己执行,所有的任务使用相同的线程
for (int i = 0; i < 5; i++) {
    exec1.execute(new MyThread()); //调用run()方法
    exec1.submit(new MyCallable()); //调用call()方法,会产生Future对象
}
exec1.shutdown();

从下面这张图可以很容易的看出submit和execute方法的区别
在这里插入图片描述

Priority (线程优先级):将线程的重要性传递给调度器,即使CPU处理线程集的顺序是不确定的,但调度器更倾向于让优先级高的线程先执行。不过优先级不会导致死锁,因为优先级较低的线程只是执行的频率较低,并不意味着它不会执行。
JDK有10个优先级即1~10,不过与多数操作系统不能映射的很好,为了使程序可移植,规定只使用MAX_PRIORITY(10)、NORM_PRIORITY(5)、MIN_PRIORITY(1)

  @Override
  public void run() {
       Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
       /*
       Thread.currentThread():表示当前线程
       Thread.currentThread().setPriority():修改线程优先级
       Thread.currentThread().getPriority():得到当前线程优先级
       */
  }

sleep(long millis) (休眠):使任务终止执行给定的时间,会抛出InterruptedException,需要在run方法中捕获,因为异常不能跨线程传播回main()

public void run() {
	Thread.yield();
	try{
     Thread.sleep(300);
	}catch(InterruptedException e){
		...
	}
}

yield() (让步):静态方法Thread.yield(),调用此方法会给线程调度器暗示:“当前线程已经执行完生命周期中最重要的部分,此时正是切换给其他任务执行的大好时机”。当然,这只是一个暗示或者说建议,没有任何机制保证它将会被采纳,并且也是建议具有相同优先级的其它线程可以运⾏。

Daemon (守护线程):也称后台线程,指程序在运行时在后台提供一种通用服务的线程,且并不属于程序中不可或缺的一部分。
所有非后台线程都终止,则此时会杀死进程中的所有后台程序;即只要有任何非后台线程还在运行,程序就不会终止。

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        try{
            while (true){
                Thread.sleep(100);
                System.out.println(Thread.currentThread()+" "+this);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            Thread daemon = new Thread(new MyRunnable());
            daemon.setDaemon(true); //必须在线程启动前,调用setDaemon方法
            daemon.start();
            System.out.println(daemon.isDaemon());
        }
        TimeUnit.MILLISECONDS.sleep(175);//等价于Thread.sleep(175);为了等待所有后台线程都启动
    }
}

运行结果:看到这个会不会有疑问,看起来先调用的start方法,为什么会先打印 是否为守护线程的判断结果 ?实际上,当调用start()方法后它会迅速返回,此时我们在调用的是不同线程run()方法,当然在它执行期间,main方法中的其他操作仍可以继续执行。
在这里插入图片描述

中断:⼀个线程执⾏完毕之后会⾃动结束,如果在运⾏过程中发⽣异常(如中断)也会提前结束。**thread.interrupt()方法会给线程设定一个标志,表明该线程已经被中断。但是下面的两次判断isInterrupted()**均为false,这是因为中断异常被捕获后会清理interrupt()方法产生的标志,因此判断时总为false。

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        try{
            while (true){
                Thread.sleep(100);
                System.out.println("running...");
            }
        } catch (InterruptedException e) {
            //判断线程是否被中断
            System.out.println(Thread.currentThread().isInterrupted());//false
        }
    }
    public static void main(String[] args) {
        Thread thread1 = new Thread(new MyRunnable());
        thread1.start();
        thread1.interrupt();//因为线程中有sleep方法,因此会抛出异常并提前结束线程
        /*中断该线程,如果该线程处于阻塞、限期等待或者⽆限期等待状态,
        *那么就会抛出InterruptedException,从⽽提前结束该线程。但是不能中断I/O阻塞和synchronized 锁阻塞。
        */
        System.out.println(Thread.currentThread().isInterrupted());//false
        System.out.println("main...");
}

在这里插入图片描述
下面再看看使用Executor时如何中断线程

public static void main(String[] args) {
    ExecutorService exec = Executors.newCachedThreadPool();
    //lambda表达式,创建一个内部匿名线程
    exec.execute(()->{
        try {
            Thread.sleep(200);
            System.out.println("running...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    //exec.shutdown();//会等待线程都执⾏完毕之后再关闭
    exec.shutdownNow();//相当于为每个线程调用interrupt()
    System.out.println("main...");

    /*如果只想中断Executor中的⼀个线程,可以通过使⽤ submit() ⽅法来提交⼀个线程,
    *它会返回⼀个Future<?> 对象,通过调⽤该对象的 cancel(true) ⽅法就可以中断线程。
    */
//        Future<?> future = exec.submit(()->{
//            ...
//        });
//        future.cancel(true);
}

在这里插入图片描述

3、共享受限资源

多个线程试图同时使用同一个资源,便可能出现访问冲突。为了解决在并发模式下线程冲突的问题,会采用序列化访问共享资源的方案,即在可能存在冲突的代码前加上锁语句(JVM实现了synchronized以及JDK提供的ReentrantLock;加上锁语句的代码称为临界区),使得在这段时间内只有一个任务可以运行这段代码。这是因为锁语句产生了相互排斥的效果,这种机制称为互斥量(mutex)

共享资源:一般是以对象形式存在的内存片段,也可能是文件、I/O端口等。

synchronized:为了控制对共享资源的访问,要先把它包装进一个对象(数据一般设为private,使其只能通过方法访问),然后把所有要访问此资源的方法标记为synchronized
对象的锁:所有对象都自动含有单一的锁(也成为监视器)。当在对象上调用任意synchronized方法(synchronized void fun() {...}),此时对象将会被加锁,其他任务要想调用该对象的同步方法,则只有等到前一个方法调用完毕并释放了锁之后才能调用。所以对于特定的对象来说,所有同步方法共享同一个对象锁。

同步代码块:它和同步方法一样,只作用于同一个实例对象,即不同的线程可以同时调用此方法且不会发生阻塞。

public class SynchronizedTest {
    public void fun1(){
        synchronized (this){ //给实例对象加锁
        	try {
                Thread.sleep(100); //为了测出不同线程执行时交替执行的现象
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        }
    }

    public static void main(String[] args) {
        SynchronizedTest s1 = new SynchronizedTest();
        SynchronizedTest s2 = new SynchronizedTest();
        ExecutorService exec = Executors.newCachedThreadPool();
        exec.execute(s1::fun1); //方法引用,与lambda等价
        exec.execute(() -> s1.fun1());
        //exec.execute(() -> s2.fun1());
    }
}
/*0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 
* 同一个对象s1同时进入同步代码块(同步方法也一样),可看到,当⼀个线程进⼊同步语句块执行操作时,另⼀个线程就必须等待。
*/
/*0 1 0 2 1 2 3 4 5 3 4 5 6 7 8 6 9 7 8 9
*s1,s2两个不同对象进入同步代码块时,可以看到两个线程交叉执行
*/

同步方法:方法返回之前,该线程会一直占用该锁。并且占用锁的线程还可以调用同一个对象的其他同步方法。缺陷:将一个大的方法声明为synchronized会影响效率。因为只读的代码不需要锁,只有修改的代码才需要。

public synchronized void fun() {
 // ...
}

同步类:它和同步静态方法一样,作用于整个类,即两个线程调⽤同⼀个类的不同对象上的同步语句,也会进⾏同步(即发生阻塞)。

public void fun2() {
    synchronized (SynchronizedTest.class){ //给类加锁
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 0; i < 10; i++) {
            System.out.print(i + " ");
        }
    }
}
public static void main(String[] args) {
    SynchronizedTest s1 = new SynchronizedTest();
    SynchronizedTest s2 = new SynchronizedTest();
    ExecutorService exec = Executors.newCachedThreadPool();

    exec.execute(()-> s1.fun2());
    exec.execute(()-> s2.fun2());
}
/*0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 
* 即两个不同线程调用同一个类的不同对象中的同步代码块,也会进行同步
*/

同步静态方法

public synchronized static void fun() {
 // ...
}

ReentrantLock(java.util.concurrent JUC包中的锁):Lock lock = new ReentrantLock(),称为显示互斥机制,即Lock对象必须被显示的创建、锁定和释放。

Lock lock = new ReentrantLock();
public void func() {
	lock.lock(); //锁定
	try {
		//...
	} finally {
		lock.unlock(); // 确保释放锁,从⽽避免发⽣死锁。
	}
}

synchronized 和 ReentrantLock 的区别

  • 实现:synchronized 是 JVM 实现的,⽽ ReentrantLock 是 JDK 实现的。
  • 性能:新版本 Java 对 synchronized 进⾏了很多优化,例如⾃旋锁等,synchronized 与 ReentrantLock ⼤致相同。
  • 等待可中断:当持有锁的线程⻓期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
    • ReentrantLock 可中断,⽽ synchronized 不⾏。
  • 公平锁:公平锁是指多个线程在等待同⼀个锁时,必须按照申请锁的时间顺序来依次获得锁。
    • synchronized 中的锁是⾮公平的,ReentrantLock 默认情况下也是⾮公平的,但是也可以是公平的。
  • 绑定多个条件:⼀个 ReentrantLock 可以同时绑定多个 Condition 对象。
  • 二者如何选择: 除⾮需要使⽤ ReentrantLock 的⾼级功能来解决特殊问题,否则优先使⽤ synchronized。
    • 这是因为 synchronized是JVM 实现的⼀种锁机制,JVM 原⽣地⽀持它,⽽ ReentrantLock 不是所有的 JDK 版本都⽀持。
    • 并且使⽤ synchronized不⽤担⼼没有释放锁⽽导致死锁问题,因为 JVM 会确保锁的释放。

原子性

原子操作不能被线程调度机制中断的操作,一旦操作开始,它就一定可以在切换到其他线程之前操作完毕。原子操作不需要进行同步控制是一个不正确的观点。

什么才是原子操作?
通常来说,对域中的值做赋值返回操作都是原子性的。原子性可以应用于除long和double之外的所有基本类型之上的简单操作(如读取和写入,注意自增++和+=不是原子性的)

volatile关键字

  • JVM将long和double的64位变量的读写操作当作两个分离的32位操作来执行,因此可能会出现在进行读写操作时发生上下文切换。为了解决这种问题,可以在定义64位变量时使用volatile,就会获得简单操作的原子性。
  • 提供了应用中的可视性。如果一个域为volatile的,那么对这个域的写操作会被立即写入到主存中(即使使用本地缓存也会如此),从而所有的读取(发生在主存中)操作都可以看到这个修改。但是,非volatile域上的原子操作不会刷新到主存中去,其他读取操作也看不到这个新值。
    • 一个任务内部的任何读写操作对于这个任务来说都是可视的,如果只需要在任务内部保持可视性,则不必将域声明为volatile
  • 如果多个任务同时访问某个域,那么这个域就应该被声明为volatile或被synchronized同步(当然同步也会导致主存刷新)
  • 当一个域依赖它之前的值如递增,或受到其他域的限制,volatile就无法工作,并且它们是非线程安全的。

专家级的程序员可以利用原子操作的性质来编写无锁(即不使用同步)代码,但通常尝试用原子操作来替换同步会带来很大的麻烦。所以使用synchronized是最安全也是首选的方式。

4、线程之间的协作以及线程状态

join()、join(millis): 在线程t1中执行t2.join(),执行到此语句会当前线程t1挂起,直到t2线程执行结束或等待millis时间,t1才会重新执行。此方法可以被interrupt()方法中断。

Object中的方法:wait()、notify()、notifyAll()

wait() 方法可以使线程等待某个条件被满足 (通常这种条件会由另一个任务来改变) 时,将线程挂起 (我们肯定不想当前任务在判断此条件时,不断进行空循环,这被称为忙等待 ),只有当其他线程调用notify()或notifyAll()时 (表明条件被满足或需要此线程被唤醒),这个挂起的线程才会被唤醒。

  • wait():线程会无限等待下去,直到收到notify()或notifyAll()消息被唤醒
  • wait(millis):在millis期间被挂起
  • notifyAll():当有多个任务处于wait()等待时,调用notifyAll()更加安全。
  • notify():是对notifyAll()的一种优化。但是,要想在合适的任务中恰当的调用notify()的条件比较苛刻,一般建议用notifyAll()。

之前在调用sleep和yield方法时,锁并不会被释放。但调用wait()方法时将释放锁,从而该对象的其他同步方法可以被另一个线程调用,而这通常会产生被挂起的任务所期待的条件变化,接着重新被唤醒。

值得注意的是,这三个方法都是Object类下的方法,不属于Thread。这也意味着这三个方法可以也只能同步方法或同步代码块中被调用(即说明调用这些方法前必须获得该对象的锁),而不必关心这个类是否继承了Thread或实现了Runnable。如果在非同步方法或代码块里调用wait()方法,可以通过编译但会抛出 IllegalMonitorStateException。

java.util.concurrent类库中的Condition类可以实现线程之间的协作: await()、signal()、signalAll() 以及Lock锁

线程状态: 调用getState()方法可得到线程当前状态

  • New(新建):线程被创建,会短暂处于这种状态。此时已经初始化并且分配到了系统资源,可以获得CPU时间,只等待调度器来将线程变为可运行或阻塞状态。
  • Runnable(可运行):线程调用**start()**方法后就处于可运行状态。只要调度器把时间片分配给线程就可以运行。即此时线程可以运行也可以不运行。
  • Blocked(阻塞):线程试图获取一个内部对象锁失败时(因为锁被其他线程占用)。此时调度器不会分配CPU时间,直到线程重新进入可运行状态。
  • Waiting(等待):当线程等待另一个线程通知调度器出现一个条件时。如调用Object.wait方法或Thread.join方法,或者是等待java.util.concurrent库中的Lock或Condition。
  • Timed waiting(计时等待):有些方法有超时参数,调用这些方法时会进入计时等待状态(这一状态将一直保持到超时期满或接受到适当的通知)。如Thread.sleep、Object.wait、Thread.join、Lock.tryLock和Condition.await。
  • Terminated(终止):run方法正常退出,线程自然终止;其他终止条件发生使线程意外终止。

死锁

导致两个或多个线程同时等待对方释放资源,都停止执行的情形称为死锁。某一个同步块同时拥有两个以上对象的锁时,可能会发生死锁问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值