多线程编程、线程同步|安全和线程通信

多线程编程

多线程的优势
线程在程序中是独立的、并发的执行流,与分隔的进程相比,进程中的线程之间的隔离程度要小。他们共享内存、文件句柄和其他每个进程应有的状态。

因为线程的划分尺度小于进程,使得多线程程序的并发性高。进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。

线程比进程具有更高的性能,这是由于同一个进程中的线程都具有共性——多个线程共享同一个进程虚拟空间。线程共享的环境包括:进程代码段、进程的共有数据等。利用这些共享的数据,线程很容易实现互相的通信。

当操作系统创建一个进程时,必须为该进程分配独立的内存空间,并分配大量的相关资源;但创建一个线程则简单的多,因此使用多线程来实现并发比使用多进程实现并发的性能要高得多。

总结起来,使用多线程变成具有以下几个优点

1.进程之间不能共享内存,但线程之间共享内存非常容易

2.系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高

3.Java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Java的多线程编程

在实际应用中,多线程是非常有用的,一个浏览器必须能同时下载多个图片;一个web服务器必须能同时响应多个用户的请求;Java虚拟机本身就在后台提供了一个超级线程来进行垃圾回收;图形用户界面(GUI)应用也需要启动单独的线程从主机环境收集用户界面事件、、,总之,多线程在实际编程中的应用是非常广泛的。

线程同步

线程安全问题

多线程编程是一件很有趣的事情,它很容易出现错误情况,这是由系统的线程调度具有一定的随机性造成的,不过即使程序偶然出现问题,那也是由于编程不当引起的。当使用多个线程来访问同一个数据时,很容易“偶然”出现线程安全问题。

扫描二维码关注公众号,回复: 8812508 查看本文章

同步代码块

当有两个进程并发修改同一个文件时就有可能出现异常,为了解决这个问题,Java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块,同步代码块的语法格式如下

synchronized(obj)
{
   //此处的代码就是同步代码块
}

obj就是同步监视器,其代码的含义就是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定

注意:任何时候只能有一个线程获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。

虽然Java程序允许使用任何对象作为同步监视器,但是同步监视器的目的:阻止两个线程对同一个共享资源进行并发访问,因此通常使用可能被并发访问的共享资源充当同步监视器。

任何线程在修改指定资源之前,首先对该资源加锁,在加锁期间其他线程无法修改该资源,当该线程修改完成后,该线程释放对该资源的锁定,通过这种方式就可以保证并发线程在任一时刻只有一个线程可以进入修改共享资源的代码区(也称为临界区),所以同一时刻最多只有一个线程处于临界区内,从而保证了线程的安全性。

同步方法

同步方法就是使用synchronized关键字来修饰某个方法,则该方法称为同步方法。对于synchronized修饰的实例方法(非static方法)而言,无须显示指定同步监视器,同步方法的同步监视器是this,也就是调用该方法的对象。

通过使用同步方法可以非常方便地实现线程安全的类,线程安全的类具有如下特征。

  • 该类的对象可以被多个线程安全的访问
  • 每个线程调用该对象的任意方法之后都将得到正确结果
  • 每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态

有可变类和不可变类,其中不可变类的线程总是安全的,因为它的对象状态不可改变;但可变对象需要额外的方法来保证其线程安全。
注意:synchronized关键字可以修饰某个方法,可以修饰代码块,但不能修饰构造器、成员变量等
同步方法的同步监视器是this,而this总代表调用该方法的对象。

可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全所带来的负面影响,程序可以采用以下策略

  • 不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源(竞争资源即共享资源)的方法进行同步,
  • 如果可变类由两种运行环境:单线程环境和多线程环境,则应该为该可变类提供两种版本,即线程不安全版本和线程安全版本。在单线程环境中使用线程不安全版本以保证性能,在多线程环境中使用线程安全版本
    JDK所提供的StringBuilder、StringBuffer就是为了照顾单线程环境和多线程环境所提供的的类,在单线程环境下使用StringBuilder来保证较好的性能;当需要保证多线程安全时,就应该使用StringBuffer)

释放同步监视器的锁定
任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,那么何时会释放对同步监视器的锁定呢?程序无法显式释放对同步监视器的锁定,线程会在如下几种情况下时释放对同步监视器的锁定

  • 当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器
  • 当前线程在同步代码块、同步方法中遇到了break、return终止了该代码块、该方法的继续执行,当前线程将会释放同步监视器
  • 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致了该代码块、该方法异常结束时,当前线程将会释放同步监视器
  • 当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器

在如下的所示情况下,线程不会释放同步监视器

  • 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器
  • 线程执行同步代码块时,其他的线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放同步监视器。并且,程序应该尽量避免使用suspend()和resume()方法来控制线程

同步锁
从Java5开始,Java提供了一种功能更强大的线程同步机制——通过显式定义同步锁对象来实现同步,在这种机制下,同步锁由Lock对象充当。
Lock提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock允许实现更灵活的结构,可以具有差别很大的属性,并且支持多个相关的Condition对象
Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象

注意:使用Lock与使用同步方法有点相似,只是使用Lock时显式使用Lock对象作为同步锁,而使用同步方法时系统隐式使用当前对象作为同步监视器,同样都符合“加锁——修改——释放锁”的操作模式,而是使用Lock对象时每个Lock对象对应一个对象,一样可以保证对于同一个对象,同一时刻只能有一个线程能进入临界区

同步方法或同步代码块使用与竞争资源相关的、隐式的同步监视器,并且强制要求加锁和释放锁要出现在同一个块结构中,并且当获取了多个锁时,他们必须以相反的顺序释放,且必须在与所有锁被获取时相同的范围内释放所有锁

虽然同步方法和同步代码块的范围机制使得多线程安全编程非常方便,而且还可以避免很多涉及锁的常见编程错误,但有时也需要以更为灵活地方式使用锁,Lock提供了同步方法和同步代码块所没有的其他功能,包括用于非块结构的tryLock()方法,以及试图获取可中断锁的lockInterruptibly()方法,还有获取超时失效锁的tryLock(long,TimeUnit)方法

ReentrantLock锁具有可重入性,也就是说,一个线程可以对已被加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个计数器来追踪lock()方法的嵌套调用,线程在每次调用lock()加锁后,必须显式调用unlock()来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。
死锁
当两个线程相互等待对方释放同步监视器时就会发生死锁,Java虚拟机没有检测,也没有采取措施来处理死锁情况,所以多线程编程时应该避免出现死锁的情况,一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。
死锁很容易发生的,尤其在系统出现多个同步监视器的情况下。

线程通信

当线程在系统内运行时,线程的调度具有一定的透明性,程序通常无法准确控制线程的轮换执行,但Java也提供了一些机制来保证线程协调运行
传统的线程通信
假设现在系统中有两个线程,这两个线程分别代表着存款者和取钱者——现在假设系统有一种特殊的要求,系统要求存款者和取钱者不断重复取钱、存款的操作,而且要求每当存款者将钱存入指定账户后,取钱者就能立即取出钱。不允许存款者连续两次存钱,也不允许取钱者连续两次取钱。
为了实现这种功能,可以借助于Object类提供的wait()、notify()和notifyAll()三个方法,这三个方法并不属于Thread类,而是属于Object。但这三个方法必须由同步监视器对象来调用,这可分成以下两种情况

  • 对于使用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用这三个方法
  • 对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象调用这三个方法

关于这三个方法解释如下:

  • wait():导致当前线程等待,直到其他线程调用该同步监视器的notify()方法或notifyAll()方法来唤醒该线程。该wait()方法有三种形式——无时间参数的wait(一直等待,知道其他线程通知)、带毫秒参数的wait()和带毫秒、毫微秒参数的wait()(这两种方法都是等待指定时间后自动苏醒)。调用wait()方法的当前线程会释放对该同步监视器的锁定。
  • notify():唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃对该同步监视器的锁定后(使用wait()方法),才可以执行被唤醒的线程。
  • notifyAll():唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程

程序中可以通过一个旗标来标识账户中是否已有存款,当旗标为false时,表明账户中没有存款,存款者线程可以向下执行,当存款者把钱存入账户后,将旗标设为true,并调用notify()或notifyAll()方法来唤醒其他线程;当存款者线程进入线程体后,如果旗标为true就调用wait()方法让该线程等待
当旗标为true时,表明账户中已经存入了存款,则取钱者线程可以向下执行,当取钱者把钱从账户中取出后,将旗标设为false,并调用notify()或notifyAll()方法来唤醒其他线程;当取钱这线程进入线程体后,如果旗标为false就调用wait()方法让该线程等待
使用Condition控制线程通信
使用阻塞队列(BlockingQueue)控制线程通信

《疯狂Java讲义》第四版李刚编著——中国工信出版集团/电子工业出版社

发布了47 篇原创文章 · 获赞 12 · 访问量 7257

猜你喜欢

转载自blog.csdn.net/weixin_43717681/article/details/102875687