进程和线程
一个进程可以有多个线程,线程有自己的堆栈,自己的程序计数器和局部变量,但是他们共享进程的
系统资源。因此要小心线程之间彼此影响。
并发编程三个特点:
- 原子性问题
- 可见性问题
- 有序性问题
多线程优点
- 进程间内存不能共享,而线程间可以共享内存
- 新建进程需要分配系统资源,而新建线程则不需要分配,创建和切换代价小,从而使多线程来实现
多任务比多进程效率更高。
java线程的创建
- 使用
Thread()
创建线程,使用start()
方法来启动线程。 - 也可以通过实现
Runnable
接口,然后作为Thread
的target创建线程。 Runnable
接口方式的优点是可以继承其他类。
线程的周期
- 创建。想让新创建的子线程立即执行可以使用
Thread.sleep(1)
,让主线程暂停一下。 - 就绪
- 运行
- 阻塞
- 死亡
tip
- 注意少使用
suspend()
和stop()
,这样会造成死锁。 - 使用
isAlive()
判断
线程的控制
- join线程
在执行的线程A运行时调用其他线程B的B.join()
方法后,A线程将等到B线程结束之后才执行。 - 后台进程
在start()
之前,调用setDaemon()
。可以使用isDaemon()
判断是否是后台线程,后台线程
创建的线程默认是后台线程。程序中的所有前台线程运行结束后,后台线程会被通知死亡,不过这会
有一定的时间。 yield()
静态方法
与sleep()
类似,但是不会让位于优先级比自己低的线程,所以如果其优先级最高且没有同级的
线程的话,调用此方法后该线程依然继续执行。- 设置线程优先级
setPriority(int)
接受1~10的整数,或者三个静态常量:MAX_PRIORITY
,MIN_PRIORITY
,NORM_PRIORITY
,对应的数值依次为10,1,5.
虽然Java提供了1~10共10个级别,但是有的系统并不支持,所以为了移植性,最佳实践是
使用常量而为直接指定数值
线程同步
同步代码块
eg. synchronized(obj)
,其中obj
为同步监视器,Java允许使用任何对象作为同步监视器,
但是由于我们的目的是阻止两条线程并发访问一个共享资源,所以最佳实践是使用可能被并发访问
的资源作为同步监视器。
同步方法
此时不需要指定同步监视器,方法会自动把this
当成同步监视器。通过同步方法,一般可以使类
成为线程安全的类,这样的类有如下特点:
- 该类的对象可以被多个线程同时访问
- 访问该对象的任意方法都能得到正确的结果
- 访问该对象的任意方法,该对象都能依然保持合理状态
可变类的线程安全是以降低程序效率为代价的,为了降低对性能的影响,需要采取以下策略:
- 减少同步的使用,只对会改变竞争资源的方法同步
- 如果可变类有两个运行环境:多线程环境和单线程环境,则可以开发线程安全和非线程安全两个
版本
释放同步监视器的锁定
程序无法显示释放对同步监视器的锁定,线程释放锁定有以下几种情况:
- 同步方法或者代码块执行完毕
- 同步方法执行到
break
,return
- 同步方法执行到
wait()
方法 - 同步方法遇到
Error
和Exception
等中止程序继续运行的情况
线程不会释放锁定的情况: - 执行
sleep()
,yield()
时 - 执行
suspend()
时
同步锁Lock
类似synchronized
的机制,Lock
常用的是ReentrantLock
。同步锁需要显示的加锁和释放。
通常在finally
里释放。ReentrantLock
具有重入性,线程可以对它已经加锁的ReentrantLock
再次加锁,所以一段被锁保护的方法可以调用另一个被相同锁保护的方法。
死锁
少使用suspend()
方法,会造成死锁。
线程通信
synchronized
方式wait()
:导致该线程等待并放弃同步监视器,知道该同步监视器的notify()
或者notifyAll()
方法来唤醒改线程notify()
:选择任意一个在该同步监视器上的线程,将其唤醒notifyAll()
:唤醒所有等待同步监视器的线程- 上述三个方法都属于
Object
而非Thread
,这三个方法都必须由同步监视器来调用。
- 同步锁方式
- 如果不使用
synchronized
而是用Lock
的话则不能再使用上述方法,此时我们使用Java提供的Condition
类保持协调。Lock
代替了同步方法或者同步块,Condition
代替了同步监视器的功能。Condition
提供了类似的三个方法: await()
:await()
还有多种变体如:awaitNanos(long)
,awaitUninterruptibly()
等signal()
signalAll()
- 如果不使用
- 使用管道流
与普通IO类似,可以使用字节流、字符流和新IO Channel三种形式。通常有共享数据就可以了,不需要使用管道流。