并发编程(一)线程基础,线程之间的共享协作
1.基础概念
1.1 什么是进程和线程
SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。进程是程序运行资源分配的最小单位。
其中资源包括:CPU、内存空间、磁盘IO等,同一进程中的多条线程共享该进程中的全部系统资源,而进程和进程之间是相互独立的。进程可以分为系统进程和用户进程。用于完成操作系统的各种功能的进程就是系统进程,他们就是处于运行状态下的操作系统本身。用户进程就是由你所启动的进程。
线程是cpu调度的最小单位,必须依赖于进程存在。
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的、能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
线程无处不在
任何一个程序都必须要创建线程,特别是Java不管任何程序都必须启动一个main函数的主线程; Java Web开发里面的定时任务、定时器、JSP和 Servlet、异步消息处理机制,远程访问接口RM等,任何一个监听事件, onclick的触发事件等都离不开线程和并发的知识。
1.2 CPU核心数和线程数的关系
多核心:指单芯片多处理器。
多线程:让同一个处理器上的多个线程同步执行并共享处理器的执行资源
核心数、线程数关系:目前主流CPU都是多核的。增加核心数目就是为了增加线程数,因为操作系统是通过线程来执行任务的,一般情况下它们是1:1对应关系,也就是说四核CPU一般拥有四个线程。但 Intel引入超线程技术后,使核心数与线程数形成1:2的关系
1.3 CPU时间片轮转机制
时间片轮转调度是一种最古老、最简单、最公平且使用最广的算法,又称RR调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
时间片设得太短会导致过多的进程切换,降低了CPU效率:而设得太长又可能引起对短的交互请求的响应变差。将时间片设为100ms通常是一个比较合理的折衷。
1.4 并行和并发
并发:指应用能够交替执行不同的任务,比如单CPU核心下执行多线程并非是同时执行多个任务,如果你开两个线程执行,就是在你几乎不可能察觉到的速度不断去切换这两个任务,已达到"同时执行效果",其实并不是的,只是计算机的速度太快,我们无法察觉到而已.
当谈论并发的时候一定要加个单位时间,也就是说单位时间内并发量是多少。离开了单位时间其实是没有意义的
并行:指应用能够同时执行不同的任务,例:吃饭的时候可以边吃饭边打电话,这两件事情可以同时执行
两者区别:一个是交替执行,一个是同时执行.
1.5 高并发编程的优点及注意事项
优点:
(1)充分利用CPU的资源
(2)加快响应用户的时间
(3)可以使你的代码模块化,异步化,简单化
注意事项:
(1)线程之间的安全性
在同一个进程里面的多线程是资源共享的,也就是都可以访问同一个内存地址当中的一个变量。例如:若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的:若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
(2)线程之间的死锁
为了解决线程之间的安全性引入了Java的锁机制,而一不小心就会产生Java线程死锁的多线程问题,因为不同的线程都在等待那些根本不可能被释放的锁,从而导致所有的工作都无法完成。假设有两个线程,分别代表两个饥饿的人,他们必须共享刀叉并轮流吃饭。他们都需要获得两个锁:共享刀和共享叉的锁。
假如线程A获得了刀,而线程B获得了叉。线程A就会进入阻塞状态来等待获得叉,而线程B则阻塞来等待线程A所拥有的刀。这只是人为设计的例子,但尽管在运行时很难探测到,这类情况却时常发生
(3)线程太多了会将服务器资源耗尽形成死机当机
线程数太多有可能造成系统创建大量线程而导致消耗完系统内存以及CPU的“过渡切换”,造成系统的死机。
OS系统对线程的数量是有限制的:Linux下默认线程数量不超过1000,而windows下默认线程数量不超过2000.当创建一个新的线程时,会为其分配一个栈空间(约1M大小),以及对应的文件描述符(句柄->指针)
某些系统资源是有限的,如文件描述符。多线程程序可能耗尽资源,因为每个线程都可能希望有一个这样的资源。如果线程数相当大,或者某个资源的侯选线程数远远超过了可用的资源数则最好使用资源池。一个最好的示例是数据库连接池。只要线程需要使用一个数据库连接,它就从池中取出一个,使用以后再将它返回池中。资源池也称为资源库。
参考:http://enjoy.ke.qq.com
2.Java线程
2.1 线程的启动与终止
线程的启动
Java中线程启动的方式有两种
1、X extends Thread;,然后X.run
2、X implements Runnable;然后交给Thread运行
参考:java.lang.Thread[1.8]
There are two ways to create a new thread of execution. One is to * declare a class to be a subclass of <code>Thread</code>. This * subclass should override the <code>run</code> method of class * <code>Thread</code>. An instance of the subclass can then be * allocated and started. For example, a thread that computes primes * larger than a stated value could be written as follows: * <hr><blockquote><pre> * class PrimeThread extends Thread { * long minPrime; * PrimeThread(long minPrime) { * this.minPrime = minPrime; * } * * public void run() { * // compute primes larger than minPrime * . . . * } * } * </pre></blockquote><hr> * <p> * The following code would then create a thread and start it running: * <blockquote><pre> * PrimeThread p = new PrimeThread(143); * p.start(); * </pre></blockquote> * <p> * The other way to create a thread is to declare a class that * implements the <code>Runnable</code> interface. That class then * implements the <code>run</code> method. An instance of the class can * then be allocated, passed as an argument when creating * <code>Thread</code>, and started. The same example in this other * style looks like the following: * <hr><blockquote><pre> * class PrimeRun implements Runnable { * long minPrime; * PrimeRun(long minPrime) { * this.minPrime = minPrime; * } * * public void run() { * // compute primes larger than minPrime * . . . * } * } * </pre></blockquote><hr> * <p> * The following code would then create a thread and start it running: * <blockquote><pre> * PrimeRun p = new PrimeRun(143); * new Thread(p).start(); * </pre></blockquote> * <p>
Thread是Java里对线程的唯一抽象,Runnable只是对任务(业务逻辑)的抽象。Thread可以接受任意一个Runnable的实例并执行。
线程的终止:
要么是run执行完成了,要么是抛出了一个未处理的异常导致线程提前结束。
stop
暂停、恢复和停止操作对应在线程Thread的API就是suspend()、resume()和stop()。但是这些API是过期的,也就是不建议使用的。不建议使用的原因主要有:以suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。正因为suspend()、resume()和stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方法。
中断
安全的中止则是其他线程通过调用某个线程A的interrupt()方法对其进行中断操作, 中断好比其他线程对该线程打了个招呼,“A,你要中断了”,不代表线程A会立即停止自己的工作,同样的A线程完全可以不理会这种中断请求。因为java里的线程是协作式的,不是抢占式的。线程通过检查自身的中断标志位是否被置为true来进行响应,
线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()来进行判断当前线程是否被中断,不过Thread.interrupted()会同时将中断标识位改写为false。
如果一个线程处于了阻塞状态(如线程调用了thread.sleep、thread.join、thread.wait等),则在线程在检查中断标示时如果发现中断标示为true,则会在这些阻塞方法调用处抛出InterruptedException异常,并且在抛出异常后会立即将线程的中断标示位清除,即重新设置为false。
不建议自定义一个取消标志位来中止线程的运行。因为run方法里有阻塞调用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取消标志。这种情况下,使用中断会更好,因为,
一、一般的阻塞方法,如sleep等本身就支持中断的检查,
二、检查中断位的状态和检查取消标志位没什么区别,用中断位的状态还可以避免声明取消标志位,减少资源的消耗。
注意:处于死锁状态的线程无法被中断
2.2 run()和start()
Thread是java对线程概念进行的抽象。new Thread() 只是创建一个Thread的实例,并没有和系统中真正的线程联系到一起。只有在执行了start()之后,才实现了真正意义上的线程启动。
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
start()方法让一个线程进入就绪队列等待分配cpu,分到cpu后才调用实现的run()方法,start()方法不能重复调用,如果重复调用会抛出异常。
run()方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方法并没有任何区别,可以重复执行,也可以被单独调用。
2.3 线程的状态和其他常用方法
线程状态
线程在新建完成执行start()后进入的是就绪状态,此时在等待时间片的分配。
yield()方法:使当前线程让出CPU占有权,但让出的时间是不可设定的。也不会释放锁资源。注意:因为又不是每个线程都需要这个锁的,而且执行yield( )的线程不一定就会持有锁。
所有执行yield()的线程有可能在进入到就绪状态后会被操作系统再次选中马上又被执行。
join方法
把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。
public class UseJoin { private static class JoinThread implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName() + "is start!"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "is end!"); } } public static void main(String[] args) throws InterruptedException { Thread main = Thread.currentThread(); Thread joinThread = new Thread(new JoinThread()); joinThread.setName("Join Thread"); joinThread.start(); joinThread.join(); System.out.println("main start..."); Thread.sleep(2000); System.out.println("main end ..."); } }
打开
joinThread.join();
结果如下:
关闭
joinThread.join();
结果如下:
线程的优先级
在Java线程中,通过一个整型成员变量priority来控制优先级,优先级的范围从1~10,在线程构建的时候可以通过setPriority(int)方法来修改优先级,默认优先级是5,优先级高的线程分配时间片的数量要多于优先级低的线程。
设置线程优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。在不同的JVM以及操作系统上,线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定。
守护线程
Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。我们一般用不上,比如垃圾回收线程就是Daemon线程。(进程停止,守护线程也将结束)
Daemon线程被用作完成支持性工作,但是在Java虚拟机退出时Daemon线程中的finally块并不一定会执行。在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。
3.线程间的共享协作
线程间的共享:
synchronized 内置锁:
线程开始运行,拥有自己的栈空间,就如同一个脚本一样,按照既定的代码一步一步地执行,直到终止。但是,每个运行中的线程,如果仅仅是孤立地运行,那么没有一点儿价值,或者说价值很少,如果多个线程能够相互配合完成工作,包括数据之间的共享,协同处理事情。这将会带来巨大的价值。
Java支持多个线程同时访问一个对象或者对象的成员变量,关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。
对象锁和类锁:
对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。
但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,类锁其实锁的是每个类的对应的class对象。类锁和对象锁之间也是互不干扰的。
(①.用于方法上->同步方法->隐式的使用 this->同步方法 ②.使用对象用于代码块上->同步块->对象锁 ③.使用在代码块上 显示的使用this ->对象锁 ->当前的类对象)
类锁本质上也是对象锁,对象为jvm中的唯一的class对象
不同的线程锁 锁不同的对象时,线程是可以并行执行的;锁相同对象时 会产生竞争谁抢到 对象(锁)谁就先执行。
