并发程序的两种模式
- 共享内存:并发的模块在内存中读写共享数据。
- 消息传递:并发的模块通过信道连接在一起,并通过信道交换信息。
线程的定义、创建方法
通过继承Thread类定义
Thread类本身的定义为
public class Thread extends Object implements Runnable
可通过继承Thread类来定义线程,并实现其中的run()
方法。
例如下面的HelloThread类:
public class HelloThread extends Thread {
public void run() {
System.out.println("Hello from a thread!");
}
public static void main(String args[]) {
HelloThread p = new HelloThread();
p.start();
//(new HelloThread()).start();//另一种创建线程的方法
}
}
从Runnable接口构造Thread对象
若使用继承Thread类的方法定义线程,则就无法继承其他的类了,所以实际应用中一般使用实现Runnable接口的方式构造类,然后再将实例传入Thread类的构造器中,构造出一个线程实例。
例如下面的HelloRunnable类:
public class HelloRunnable implements Runnable {
public void run() {
System.out.println("Hello from a thread!");
}
public static void main(String args[]) {
(new Thread(new HelloRunnable())).start();
}
}
注:创建线程的唯一方法是调用Thread实例的start()方法
即使调用了run()
方法,虽然程序也会运行,但无法创建线程,只能通过调用start()
方法来创建线程。
交错和竞争
- 原子操作不会导致竞争
- 单行、单条语句未必是原子的
- 是否是原子操作由JVM确定
例如课上的例子balance = balance + 1
,这条语句在执行时被分解为三步:
- 读取
balance
的值 - 运算
balance + 1
- 将新值赋值给
balance
】
调用方法影响线程之间的interleave关系
sleep()方法
可以使线程进行休眠,不会失去锁的所有权。
interrupt()方法
向线程发送中断信号,但中断与否由线程自己处理。
若线程为t,则相关的方法有:
- t.interrupt():在其他线程中向t发出中断信号。
- t.isInterrupted():返回t线程是否被其他线程中断。
- t.interrupted():返回t线程是否被其他线程中断,并若被中断,则取消它的中断状态。
sleep()方法和interrupt()方法之间的冲突
当线程sleep()时收到中断信号,则会立即结束休眠,并抛出InterruptedException异常;当线程被中断时执行到sleep()语句,也会抛出InterruptedException异常。
join()方法
join()方法用于说明两个线程的先后关系。例如A线程调用B.join(),则是让B线程临时插队,完成B线程任务后再转移回A线程。例如课上的例子中,首先启动了th1线程,然后在主线程中对th1线程调用了join方法,意味着主线程不再继续往下执行,而是要等到th1线程结束再继续执行。后面的th2,th3同理。这是上课时老师没有讲清楚的。
public class JoinExample {
public static void main(String[] args) {
Thread th1 = new Thread(new Runnable(){public void run(){System.out.println("th1");}}, "th1");
Thread th2 = new Thread(new Runnable(){public void run(){System.out.println("th2");}}, "th2");
Thread th3 = new Thread(new Runnable(){public void run(){System.out.println("th3");}}, "th3");
th1.start();
try{
th1.join();
}catch(InterruptedException e){}
th2.start();
try{
th2.join();
}catch(InterruptedException e){}
th3.start();
try{
th3.join();
}catch(InterruptedException e){}
}
}
线程安全的方法
限制数据共享
线程之间不共享数据,避免使用全局变量(包括静态变量)。
共享不可变数据
- 使用不可变数据类型和不可变引用,避免多个线程之间的竞争。
- 不可变数据类型通常是线程安全的。
共享线程安全的可变数据
JDK通常同时提供两个相同功能的类,一个是线程安全的,另一个不是。
- StringBuffer线程安全,StringBuilder不安全。
- 集合类都是线程不安全的。
- 可通过Collections.synchronizedMap/List/Set()方法将集合类变为线程安全的(注:要把参数引用销毁)。
- 在线程安全的集合类上使用iterator仍是不安全的。
- 多个操作放在一次仍不安全。
- 能修改的变量都是线程不安全的。
运用线程锁
锁与同步
锁可理解为将多个操作变为一个原子操作。例如:
synchronized (lock) {
balance = balance + 1;
}
- 要保证操作互斥,必须将所有能接触到数据的方法用同一个锁锁住。
以下两种锁的方式等价:
public void test(){
synchronized (this){
a = a + 1;
}
}
public synchronized void test(){
a = a + 1;
}
- 静态方法上锁意味着在类的层面上上锁,其他线程无法访问该类的任何实例。
死锁:多个线程竞争锁,相互等待对方释放。
出现原因: 多个线程使用了多个锁且访问锁的顺序不同。
例如:
T1:
synchronized(a){
synchronized(b){
…
}
}
T2:
synchronized(b){
synchronized(a){
…
}
}
例如上课时讲的:
import java.util.HashSet;
import java.util.Set;
public class Person {
private final Set<Person> friends = new HashSet<>();
public static void main(String[] args) {
Person a = new Person();
Person b = new Person();
Thread th1 = new Thread(new Runnable(){public void run(){a.friend(b);a.defriend(b);}}, "th1");
Thread th2 = new Thread(new Runnable(){public void run(){b.friend(a);b.defriend(a);}}, "th2");
th1.start();
th2.start();
}
public synchronized void friend(Person that){
if(friends.add(that)) that.friend(this);
}
public synchronized void defriend(Person that){
if(friends.remove(that)) that.defriend(this);
}
}
实际运行结果:
解决方法:改变锁的顺序。
效率问题:排序 需提前知道要使用哪些锁
解决方案:多个锁变为一个锁,细粒变为粗粒。
例如使用多个锁的共性特征(如在同一个集合中)作为新的锁。
wait(), notify(), notifyAll()
在某些条件需要满足时,为了避免重复的while()等待,可暂时挂起该线程,待条件满足后再唤醒。
wait()
操作只能在synchronized
中调用,并且调用的实例需与锁相同。o.wait()
操作代表将该线程阻塞,并将该线程添加到o对象的等待队列中。o.notify()
操作将o对象等待队列中的随机一个线程唤醒。o.notifyAll()
操作将o对象等待队列中的所有线程唤醒。
Comments