JAVA并发入门与多线程介绍

1、并发性与多线程介绍
在过去单 CPU 时代,单任务在一个时间点只能执行单一程序。之后发展到多任务阶段,计算机能在同一时间点并行执行多任务或多进程。虽然并不是真正意义上的“同一时间点”,而是多个任务或进程共享一个CPU,并交由操作系统来完成多任务间对CPU的运行切换,以使得每个任务都有机会获得一定的时间片运行。

随着多任务对软件开发者带来的新挑战,程序不再能假设独占所有的CPU时间、所有的内存和其他计算机资源。一个好的程序的榜样是在其不再使用这些资源时对其进行释放,以使得其他程序能有机会使用这些资源。

再后来发展到多线程技术,使得在一个程序内部能拥有多个线程并行执行。一个线程的执行可以被认为是一个 CPU 在执行该程序。当一个程序运行在多线程下,就好像有多个 CPU 在同时执行该程序。

多线程比多任务更加有挑战。多线程是在同一个程序内部并行执行,因此会对相同的内存空间进行并发读写操作。这可能是在单线程程序中从来不会遇到的问题。其中的一些错误也未必会在单 CPU 机器上出现,因为两个线程从来不会得到真正的并行执行。然而,更现代的计算机伴随着多核CPU的出现,也就意味着不同的线程能被不同的 CPU 核得到真正意义的并行执行。

如果一个线程在读一个内存时,另一个线程正向该内存进行写操作,那进行读操作的那个线程将获得什么结果呢?是写操作之前旧的值?还是写操作成功之后的新值?或是一半新一半旧的值?或者,如果是两个线程同时写同一个内存,在操作完成后将会是什么结果呢?是第一个线程写入的值?还是第二个线程写入的值?还是两个线程写入的一个混合值?因此如没有合适的预防措施,任何结果都是可能的。而且这种行为的发生甚至不能预测,所以结果也是不确定性的。

2、JAVA并发

Java是一种多线程语言。虽然编程问题中相当大的一部分都可以通过使用顺序编程来解决。然而,对于某些问题,如果能够并行地执行程序中的多个部分,则会变得非常方便甚至非常必要,因为这些部分要么看起来在并发地执行,要么在多处理器环境下可以同时执行。

并发具有可论证的确定性,但是实际上具有不可确定性。偏偏在我们又无法避免使用线程的代码。例如,Web系统是最常见的Java应用系统之一,而基本的Web类库、Servlet具有天生的的多线程。图形化用户界面也是类似的情况。

并发通常可以提高运行在单处理器上的程序的性能。这听起来有些违背直觉。如果你仔细考虑一下就会发现,在单处理器上运行的并发程序开销确实应该比该程序的所有部分都顺序执行的开销大,因为其中增加了所谓的上下文切换的代价。但为什么还要使用呢?问题的关键—阻塞。如果程序中的某个任务因为该程序控制范围之外的某些条件(通常是I/O)而导致不能继续执行。没有并发,整个计划接近停止,直到外界条件的变化。然而,如果程序使用并发,当一个任务被堵住了其他任务的程序可以继续执行,所以该计划继续前进。事实上,从性能的角度来看,如果没有任务会阻塞,那么在单处理器机器上使用并发就没有任何意义。

Java的线程机制是抢占式的(因为当今大部分操作系统都是抢占式的)。这表示调度机制会周期性地中断线程,将上下文切换到另一个线程,从而为每个线程都提供时间片,使得每个线程都会分配到数量合理的时间去驱动它的任务。

并发需要付出代价,包含复杂性代价,但是这些代价与在程序设计、资源负载均衡以及用户方便使用的方面改进相比,就显得微不足道了。通常,线程使你能够创建更加松散耦合的设计,否则,你的代码中各个部分都必须显式地关注那些通常可以由线程来处理的任务。

并行与并发
并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。
并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。

线程安全:经常用来描绘一段代码。指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。这个时候使用多线程,我们只需要关注系统的内存,cpu是否够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果,如不加事务的转账代码:

void transferMoney(User from, User to, float amount){
  to.setMoney(to.getBalance() + amount);//转账到的账号+
  from.setMoney(from.getBalance() - amount);//转出的账号-
}

**同步:**Java中的同步指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。如上面的代码简单加入@synchronized关键字。在保证结果准确的同时,提高性能,才是优秀的程序。线程安全的优先级高于性能。

3、基本的线程机制

线程和进程的区别
系统要做一件事,运行一个任务,所有运行的任务通常就是一个程序。每个运行中的程序就是一个进程,这一点在任务管理器上面可以形象地看到。当一个程序运行时,内部可能会包含多个顺序执行流,每个顺序执行流就是一个线程。

线程模型为编程带来了便利,它简化了在单一程序中同时交织在一起的多个操作的处理。在使用线程时,CPU将轮流给每个任务分配其占用时间。使用线程机制是一种建立透明的、可扩展程序的方法。如果程序运行得太慢,为机器增添一个CPU就能很容易地加快程序的运行速度。多任务和多线程往往是使用多处理器系统的最合理方式。

线程的状态
这里写图片描述
各种状态一目了然,值得一提的是”blocked”这个状态:
线程在Running的过程中可能会遇到阻塞(Blocked)情况

2、调用wait(),使该线程处于等待池(wait blocked pool),直到notify()/notifyAll(),线程被唤醒被放到锁定池(lock blocked pool ),释放同步锁使线程回到可运行状态(Runnable)

3、对Running状态的线程加同步锁(Synchronized)使其进入(lock blocked pool ),同步锁被释放进入可运行状态(Runnable)。

此外,在runnable状态的线程是处于被调度的线程,此时的调度顺序是不一定的。Thread类中的yield方法可以让一个running状态的线程转入runnable。

synchronized、wait、notify
它们是应用于同步问题的人工线程调度工具。讲其本质,首先就要明确monitor的概念,Java中的每个对象都有一个监视器,来监测并发代码的重入。在非多线程编码时该监视器不发挥作用,反之如果在synchronized 范围内,监视器发挥作用。

wait/notify必须存在于synchronized块中。并且,这三个关键字针对的是同一个监视器(某对象的监视器)。这意味着wait之后,其他线程可以进入同步块执行。

当某代码并不持有监视器的使用权时(即脱离同步块)去wait或notify,会抛出java.lang.IllegalMonitorStateException。也包括在synchronized块中去调用另一个对象的wait/notify,因为不同对象的监视器不同,同样会抛出此异常。

再讲用法:
1、synchronized单独使用:
如下,在多线程环境下,synchronized块中的方法获取了lock实例的monitor,如果实例相同,那么只有一个线程能执行该块内容。

public class Thread1 implements Runnable {
   Object lock;
   public void run() {  
       synchronized(lock){
         ..do something
       }
   }
}

2、直接用于方法:
相当于上面代码中用lock来锁定的效果,实际获取的是Thread1类的monitor。更进一步,如果修饰的是static方法,则锁定该类所有实例。

public class Thread1 implements Runnable {
   public synchronized void run() {  
        ..do something
   }
}

3、synchronized, wait, notify结合:典型场景生产者消费者问题:

/**
   * 生产者生产出来的产品交给店员
   */
  public synchronized void produce()
  {
      if(this.product >= MAX_PRODUCT)
      {
          try
          {
              wait();  
              System.out.println("产品已满,请稍候再生产");
          }
          catch(InterruptedException e)
          {
              e.printStackTrace();
          }
          return;
      }

      this.product++;
      System.out.println("生产者生产第" + this.product + "个产品.");
      notifyAll();   //通知等待区的消费者可以取出产品了
  }

  /**
   * 消费者从店员取产品
   */
  public synchronized void consume()
  {
      if(this.product <= MIN_PRODUCT)
      {
          try 
          {
              wait(); 
              System.out.println("缺货,稍候再取");
          } 
          catch (InterruptedException e) 
          {
              e.printStackTrace();
          }
          return;
      }

      System.out.println("消费者取走了第" + this.product + "个产品.");
      this.product--;
      notifyAll();   //通知等待去的生产者可以生产产品了
  }

volatile
多线程的内存模型:main memory(主存)、working memory(线程栈),在处理数据时,线程会把值从主存load到本地栈,完成操作后再save回去。
volatile关键词的作用:每次针对该变量的操作都激发一次load and save。
这里写图片描述
针对多线程使用的变量如果不是volatile或者final修饰的,很有可能产生不可预知的结果(另一个线程修改了这个值,但是之后在某线程看到的是修改之前的值)。
其实道理上讲同一实例的同一属性本身只有一个副本。但是多线程是会缓存值的,本质上,volatile就是不去缓存,直接取值。但是,在线程安全的情况下加volatile会牺牲性能。

线程创建
Java实现线程的方式有两种:

·继承java.lang.Thread,并重写它的run()方法,将线程的执行主体放入其中。
·实现java.lang.Runnable接口,实现它的run()方法,并将线程的执行主体放入其中。

这两种实现方式的区别并不大。继承Thread的类直接创建对象,start()启动线程,而实现Runnable接口的类需要创建对象后转型成Thread再start()启动。

继承Thread类的方式实现起来较为简单,但是继承它的类就不能再继承别的类了,因此也就不能继承别的类的有用的方法了。而使用Runnable接口的方式就不存在这个问题了,而且这种实现方式将线程主体和线程对象本身分离开来,逻辑上也较为清晰,并且通过Thread类来启动Runnable实现的多线程可以实现资源共享。所以推荐大家更多地采用这种方式。

基本线程类
基本线程类指的是Thread类,Runnable接口,Callable接口,Future接口,FutureTask接口。
Thread 类实现了Runnable接口,启动一个线程的方法:

MyThread my = new MyThread();
  my.start();

Thread类相关方法:

//当前线程可转让cpu控制权,让别的就绪状态线程运行(切换)
public static Thread.yield() 
//暂停一段时间
public static Thread.sleep()  
//在一个线程中调用other.join(),将等待other执行完后才继续本线程。    
public join()
//后两个函数皆可以被打断
public interrupte()

关于中断:它并不像stop方法那样会中断一个正在运行的线程。线程会不时地检测中断标识位,以判断线程是否应该被中断(中断标识值是否为true)。中断只会影响到wait状态、sleep状态和join状态。被打断的线程会抛出InterruptedException。

Thread.interrupted()检查当前线程是否发生中断,返回boolean。

synchronized在获锁的过程中是不能被中断的。

中断是一个状态!interrupt()方法只是将这个状态置为true而已。所以说正常运行的程序不去检测状态,就不会终止,而wait等阻塞方法会去检查并抛出异常。如果在正常运行的程序中添加while(!Thread.interrupted()) ,则同样可以在中断后离开代码体。
Runnable接口
与Thread类似,区别上文已述。

Callable接口
Callable与Runnable的功能大致相似,Callable中有一个call()函数,但是call()函数有返回值,而Runnable的run()函数不能将结果返回给客户程序。Callable的声明如下 :

public interface Callable<V> {  
    /** 
     * Computes a result, or throws an exception if unable to do so. 
     * 
     * @return computed result 
     * @throws Exception if unable to compute a result 
     */  
    V call() throws Exception;  
}  

可以看到,这是一个泛型接口,call()函数返回的类型就是客户程序传递进来的V类型。

future接口

并发模式的一种,可以有两种形式,即无阻塞和阻塞,分别是isDone和get方法。其中Future对象用来存放该线程的返回值以及状态。
Executor就是Runnable和Callable的调度容器,Future就是对于具体的Runnable或者Callable任务的执行结果进行取消cancel()、查询是否完成isDone()、获取结果get()。

ExecutorService e = Executors.newFixedThreadPool(3);
 //submit方法有多重参数版本,及支持callable也能够支持runnable接口类型.
Future future = e.submit(new myCallable());
future.isDone() //return true/false 无阻塞,查询是否完成
future.get() // return 阻塞直到该线程运行结束,返回结果

FutureTask接口
FutureTask接口实现RunnableFuture接口

public class FutureTask<V> implements RunnableFuture<V>  

RunnableFuture接口又继承了Runnable, Future这两个接口。

public interface RunnableFuture<V> extends Runnable, Future<V> {  
    /** 
     * Sets this Future to the result of its computation 
     * unless it has been cancelled. 
     */  
    void run();  
}  

线程启动
要启动一个线程,必须调用方法来启动它,这个方法就是Thread类的start()方法。

当我们采用实现Runnable接口的方式来实现线程的情况下,在调用new Thread(Runnable target)构造器时,将实现Runnable接口的类的实例设置成了线程要执行的主体所属的目标对象target,当线程启动时,这个实例的run()方法就被执行了。当我们采用继承Thread的方式实现线程时,线程的这个run()方法被重写了,所以当线程启动时,执行的是这个对象自身的run()方法。总结起来就一句话,线程类有一个Runnable类型的target属性,它是线程启动后要执行的run()方法所属的主体,如果我们采用的是继承Thread类的方式,那么这个target就是线程对象自身,如果我们采用的是实现Runnable接口的方式,那么这个target就是实现了Runnable接口的类的实例。

线程实现

public class LiftOff implements Runnable { //实现Runnable接口
    protected int countDown = 10;
    private static int taskCount = 0;
    private final int id = taskCount++;

    public LiftOff() {
    }

    public LiftOff(int countDown) {
        this.countDown = countDown;
    }

    public String status() {
        return "#" + id + "(" + (countDown > 0 ? countDown : "LiftOff!") + "),";
    }

    public void run() {
        while(countDown-->0){
            System.out.print(status());
            Thread.yield();
        }
    }
}
public class BasicThreads {
    public static void main(String[] args){
//      LiftOff lift = new LiftOff();//这样不会实现线程行为,必须显式地将任务附到线程上
//      lift.run();

        LiftOff lift = new LiftOff();
        Thread t = new Thread(lift);
        t.start();//start()迅速返回了
        System.out.println("Waiting for LiftOff");
    }
}

结果:
这里写图片描述

尽管start()看起来像是产生了对一个长期运行方法的调用,但是从输出中可以看到,start()迅速返回了,因为打印信息Waiting for LiftOff消息在倒计时完成之前就出现了。

实际上,你产生的是对LiftOff.run()方法调用,并且这个方法还没有完成,但是因为LiftOff.run()是由不同的线程执行的,因此你仍旧可以执行main()线程中的其他操作(这种能力并不局限于main()线程,任何线程都可以启动另一个线程)。因此,程序会同时运行两种方法,main()和LiftOff()是程序中与其他线程“同时”执行的代码。

你可以很容易地添加更多线程去驱动更多的任务。下面,你可以看到所有任务彼此之间是如何互相呼应的(更换主函数):

public class MoreBasicThreads {
    public static void main(String[] args){
        for(int i=0;i<5;i++){
            new Thread(new LiftOff()).start();
        }
        System.out.println("Waiting for LiftOff");
    }
}

结果:
Waiting for LiftOff
@1(9),@2(9),@0(9),@3(9),@4(9),@1(8),@2(8),@0(8),@3(8),@1(7),
@4(8),@2(7),@0(7),@3(7),@1(6),@4(7),@2(6),@3(6),@0(6),@1(5),
@4(6),@3(5),@0(5),@2(5),@1(4),@4(5),@3(4),@0(4),@2(4),@1(3),
@4(4),@3(3),@0(3),@2(3),@1(2),@4(3),@3(2),@0(2),@2(2),@1(1),
@4(2),@3(1),@0(1),@2(1),@1(LiftOff!),@4(1),@3(LiftOff!),@0(LiftOff!),@2(LiftOff!),@4(LiftOff!),

输出说明不同任务的执行在线程被换进换出时混在了一起。这种交换是由线程调度器自动控制的。如果你的机器上有多个处理器,线程调度器将会在这些处理器之间默默地分发线程。这个程序的一次运行结果可能与领域次运行的结果不同,因为线程调度机制是非确定性的。

当main()创建Thread对象时,它并没有持有任何对这些对象的引用。在使用普通对象时,对于垃圾回收来说是一场公平的游戏,但是在使用Thread时,情况就不同了。每个Thread都“注册”了它自己,因此确实有一个对它的引用,而且在它的任务退出其run()并死亡之前,垃圾回收器无法清除它。你可以从输出中看到,这些任务确实运行到了结束,因此一个线程会创建一个单独的执行线程,在对start()调用完成之后,它仍旧会继续存在。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值