进程和线程
都是一段可执行的代码,进程是系统进行资源分配和调度的一个独立单位,线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位,线程自己基本上不拥有系统资源,只拥有一点在运行中不可少的资源。一个进程可拥有多个线程,多个线程共享这个进程上的全部资源。进程和线程都是一个时间段的描述,是CPU工作时间段的描述。
实现线程的三种方式
使用内核线程实现、使用用户线程实现、使用用户线程加轻量级进程混合实现。
内核线程:直接由操作系统内核支持的线程,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上,每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就叫做多线程内核。程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程。轻量级进程就是我们通常意义上所讲的线程。
狭义上用户线程指完全建立在用户空间的线程库上,系统内核不能感知线程存在的实现。如果程序实现得当,这种线程不需要切换到内核态,因此操作可以是非常快速且低消耗的。
3.使用用户线程加轻量级进程混合实现
线程调度是指系统的线程分配处理器使用权的过程,主要调度方式有两种:协同式线程调度、抢占式线程调度。
6种线程状态:新建(new)、运行(Runnable)、无限期等待(Waiting)、限期等待、阻塞、结束。
阻塞:“阻塞状态”和“等待状态”的区别是:“阻塞状态”在等待着获取到一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间,或者唤醒动作的发生,在程序等待进入同步区域的时候,线程将进入这种状态。
线程安全与锁优化
java语言中各种操作共享的数据分为5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
相对线程安全:我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
线程兼容:指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用。
1)互斥同步:同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个线程使用,而互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题,其次,同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。
除了synchronized,还可以使用java.util.concurrent包中的重入锁(Reentrantlock)来实现同步。
2)非阻塞同步
3)无同步方案:1、可重入代码:相对线程安全来说,可重入更基本的特征,它可以保证线程安全,即所有的可重入的代码都是线程安全的,但是并非所有的线程安全的代码都是可重入的。
2、线程本地存储
volatile关键字的作用
1、保证此变量对所有线程的可见性,可见性指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得到的。
深入理解Java线程池
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
Java中通过线程池来达到使线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务,核心ThreadPoolExecutor
ThreadPoolExecutor类中有几个非常重要的方法:execute()、submit()、shutdown()、shutdownNow()。
execute()是核心方法。本是Executor中声明的方法,通过该方法可以向线程池提交一个任务,交由线程池去执行。
corePoolSize:核心池大小(即线程池中的线程数目大于这个参数时,提交的任务会被放进任务缓存队列)。
maximumPoolSize:线程池最大能容纳的线程数。
任务提交给线程池之后的处理策略:
1)如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
2)如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说就是任务缓存队列已满),则会尝试创建新的线程去执行这个任务。
3)如果当前线程中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理。
4)如果线程池中的线程数量大于corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize,如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程池也会被终止。
实现多线程有两种实现方法:一种是继承Thread类,另一种是实现Runnable接口。
实现Runnable接口相比继承Thread类有如下优势:
- 可以避免由于Java的单继承特性而带来的局限;
- 增强程序的健壮性,代码能够被多个程序共享,代码与数据是独立的;
- 适合多个相同程序代码的线程区处理同一资源的情况。
Java5之后出现第三种方式:实现Callable接口。
线程中断
当一个线程运行时,另一线程可以调用对应的Thread对象的interrupt()方法来中断它,该方法只是在目标线程中设置一个标志,表示它已经被中断,并立即返回。
yield和join方法的使用
1)join方法用线程对象调用,如果在一个线程A中调用另一个线程B的join方法,线程A将会等待线程B执行完毕后再执行。
2)yield方法可以直接用Thread类调用,yield让出CPU执行权给同等级的线程,如果没有相同级别的线程在等待CPU的执行权,则该线程继续执行。
两类线程
Java中有两类线程:User Thread(用户线程)和Daemon Thread(守护线程)
用户线程即运行在前台的线程,而守护线程是运行在后台的线程。
守护线程作用是为其他前台线程的运行提供便利服务,而且仅在普通、非守护线程仍然运行时才需要。垃圾回收线程就是一个守护线程。
可以用Thread的setDaemon(true)方法设置当前线程为守护线程。
不要在守护线程中执行业务逻辑操作(比如对数据的读写等)。
1、setDaemon(true)必须在调用线程的start()方法之前设置;
2、在守护线程中产生的新线程也是守护线程;
3、不要认为所有的应用都可以分配给守护线程来进行服务,比如读写操作或者计算逻辑。
线程阻塞
线程可以阻塞于四种状态:
1、当线程执行Thread.sleep()时,它一直阻塞到指定的毫秒时间之后,或者阻塞被另一个线程打断;
2、当线程碰到一条wait()语句时,它会一直阻塞到接到通知(notify)、被中断或经过了指定毫秒时间为止(若指定了超时值的话)。
3、线程阻塞与不同的I/O的方式有多种,常见的一种是InputStream的read()方法,该方法一直阻塞到从流中读取一个字节的数据为止,它可以无限阻塞,因此不能指定超时时间。
4、线程池也可以阻塞等待获取某个对象锁的排他性访问权限(即等待获得synchronized语句必须的锁时阻塞)。
并非所有的阻塞状态都是可中断的,以上阻塞状态的前两种可以被中断,后两种不会对中断做出反应。
在Collections类中有多个静态方法,它们可以获取通过同步方法封装非同步集合而得到的集合:
public static Collection synchronizedCollection(Collection c);
public static List synchronizedList(List l);
public static Map synchronizedMap(Map m);
public static Set synchronizedSet(Set s);
public static SortedMap synchronizedSortedMap(SortedMap sm);
public static SortedSet synchronizedSortedSet(SortedSet ss);
死锁
当线程需要同时持有多个锁时,有可能产生死锁。
线程A当前持有互斥锁lock1,线程B当前持有互斥锁lock2。当线程A仍然持有lock1时,它试图获取lock2,因为线程B正持有lock2,因此线程A会阻塞等待线程B对lock2的释放。如果此时线程B在持有lock2的时候,也在试图获取lock1,因为线程A正持有lock1,因此线程B会阻塞等待A对lock1的释放。二者都在等待对方所持有锁的释放,而二者却又都没释放自己所持有的锁,这时二者便会一直阻塞下去,这种情况称为死锁。
避免死锁是一件困难的事,遵循以下原则有助于规避死锁:
1、只在必要的最短时间内持有锁,考虑使用同步语句代替整个同步方法;
2、尽量编写不在同一时刻需要持有多个锁的代码,如果不可避免,则确保线程持有第二个锁的时间尽量短暂;
3、创建和使用一个大锁来代替若干小锁,并把这个锁用于互斥,而不是用作单个对象的对象级别锁。
可重入内置锁
每个Java对象都可以用作一个实现同步的锁,这些锁被称为内置锁或监视器锁。获得内置锁的唯一途径进入由这个锁保护的同步代码块或方法。
Java NIO(New IO)
Java NIO是一个可以替代标准Java IO API的IO API。
标准的IO基于字节流和字符流进行操作,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入通道也类似。
Java NIO可以让你非阻塞的使用IO,Java NIO引入了选择器的概念,选择器用于监听多个通道的事件。
NIO由以下核心部分组成:Channels、Buffers、Selectors
Selector允许单线程处理多个Channel,如果你的应用打开了多个连接(通道),但每一个连接的流量都很低,使用Selector就会很方便。
要使用Selector,得向Selector注册Channel,然后调用它的select()方法,这个方法会一直阻塞到某个注册的通道有事件就绪。
Java NIO的通道类似流,但又有些不同:
1)既可以从通道中读取数据,又可以写数据到通道,但流的读写通常是单向的;
2)通道可以异步的读写;
3)通道的数据总是要先读到一个Buffer,或者总要从一个Buffer中写入。
FileChannel从文件中读取数据
DataChannel能通过UDP读写网络中的数据
SocketChannel能通过TCP读写网络中的数据
ServerSocketChannel可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
Buffer:缓冲区本质是一块可以写入数据,然后可以从中读取数据的内存。
这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便地访问这块内存。使用Buffer读写数据四个步骤:
1、写入数据到Buffer;
2、调用flip()方法;
3、从Buffer中读取数据;
4、调用clear()方法或者compact()方法。