多进程与多线程的区别
每个进程拥有自己的一整套变量,而线程则共享数据。共享变量使线程之间的通信比进程之间的通信更有效,更容易。在有些操作系统中,与进程相比,线程更“轻量级”,创建、撤销一个线程的开销远比进程开销小。
创建线程的方式
第一种方法是构建一个Thread类的子类并重写run方法,这种方法是不推荐的,应该将并行运行任务与运行机制解耦。
class MyThread extends Thread{
@Override
public void run(){...}
}
MyThread t=new MyThread();
第二种方法是实现Runnable接口,这种方法也更加灵活,当一个类继承了其他类时也可以通过实现该接口定义为线程,第一种方法的话由于不能多继承因此继承了其他类就不能定义为线程了。
class MyThread implements Runnable{
@Override
public void run(){...}
}
MyThread r=new MyThread();
//将实现了Runnable接口的类实例作为参数传递创建线程
Thread t=new Thread(r);
//也可通过匿名内部类创建线程
Thread t=new Thread(new Runnable() {
@Override
public void run() {
...
}
});
//或者用lambda表达式
Thread t=new Thread(()->{
...
});
以上创建线程的实际效果都是一样的
线程状态
新创建(New) :用new操作符创建一个新线程
可运行(Runnable):调用start方法后线程处于runnable,可能运行也可能没有
被阻塞(Blocked):内部锁获取失败将被阻塞
等待(Waiting):等待其他线程通知 wait/join等
计时等待(Time Waiting):带超时参数的等待 sleep/wait/join等
终止(Terminated):run方法正常推出/未捕获异常终止了run方法
线程属性
线程优先级 可以用setPriority(int n)设置线程的优先级
在Linux中的JVM里线程优先级相同
yield方法是一个静态方法,它可以使当前线程处于让步状态,如果其他线程的优先级大于等于它,就优先调度。类似欺软怕硬。。
守护线程:调用setDaemon(true)将线程转换为守护线程,唯一的目的是为其他线程服务,只剩守护线程时JVM就退出了。setDaemon方法必须在线程启动前调用
同步
两个或以上线程需要共享对同一数据的存取,这样可能会发发生一些错误
举个例子
一个银行类
public class Bank {
private final double[] accounts;
public Bank(int n,double initBalance){
accounts=new double[n];
Arrays.fill(accounts,initBalance);
}
public void transfer(int from,int to,double amount) throws InterruptedException {
while (true){
System.out.print("当前线程:" + Thread.currentThread());
accounts[from] -= amount;
System.out.printf("%10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf("总额:%10.2f ", getSum());
System.out.print(accounts[from]+" "+accounts[to]);
System.out.println();
}
}
public double getSum(){
double sum=0;
for (double account : accounts) {
sum+=account;
}
return sum;
}
public int size(){return accounts.length;}
}
测试类
public class UnsyncTest {
public static final int NUM=10;
public static final double INIT_BALANCE=1000;
public static final double MAX_AMOUNT=1000;
public static final int DELAY=100;
public static void main(String[] args) {
Bank bank=new Bank(NUM,INIT_BALANCE);
for(int i=0;i<NUM;i++){
int from=i;
Runnable r= () -> {
try{
while (true){
int to=(int)(bank.size()*Math.random());
double amount=MAX_AMOUNT*Math.random();
bank.transfer(from,to,amount);
Thread.sleep((int)(DELAY*Math.random()));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread t=new Thread(r);
t.start();
}
}
}
以上代码实现的是一个多线程随机转账的效果 虽然无法确定某一时刻某一账户有多少钱,但是所有账户的总金额是不变的,但是以上代码在运行一段时间后总金额可能会下降。
原因是accounts[to] += amount;不是原子性的,该指令可能被处理如下:将accounts[to] 加载到寄存器,增加amount,然后结果写回accounts[to] 。
假设线程1读取到账户有5000,增加500变成5500。(还没写回)
然后假设此时线程2读取了账户有5000(没写回所以还是5000),然后增加了900变成了5900并写回。
此时线程1完成写回5500,这样错误就发生了。
ReentrantLock锁
我们可以用reentrantLock锁来解决上述问题
使用锁可以保证在任何时刻只有一个线程进入临界区(访问临界资源的代码),在本例中就是保证只有一个线程来完成转账然后才轮到别人。
public class Bank {
private final double[] accounts;
private Lock lock=new ReentrantLock();//定义一个锁
public Bank(int n,double initBalance){
accounts=new double[n];
Arrays.fill(accounts,initBalance);
}
public void transfer(int from,int to,double amount) throws InterruptedException {
lock.lock();//加锁
try{
System.out.print("当前线程:" + Thread.currentThread());
accounts[from] -= amount;
System.out.printf("%10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf("总额:%10.2f ", getSum());
System.out.print(accounts[from]+" "+accounts[to]);
System.out.println();
}finally{
lock.unlock();//解锁
}
}
public double getSum(){
double sum=0;
for (double account : accounts) {
sum+=account;
}
return sum;
}
public int size(){return accounts.length;}
}
如此一来加锁和解锁之间的代码就被保护起来了。
锁是可重入的,因为线程可以重复获得已持有的锁,锁保持一个持有计数来跟踪对lock方法的嵌套调用,线程在每次lock后都要unlock来释放锁。被一个锁保护的代码可以调用另一个使用相同锁的方法,例如transfer方法调用getSum方法,因此lock的持有计数为2,两个方法都执行完时才能释放锁。
条件对象
线程进入临界区,发现需要满足某一条件后才能运行,使用条件对象来管理那些已获得一个锁但不能做有用工作的线程。
public class Bank {
private final double[] accounts;
private Lock lock=new ReentrantLock();
private Condition condition;//创建一个条件对象
public Bank(int n,double initBalance){
accounts=new double[n];
Arrays.fill(accounts,initBalance);
condition=lock.newCondition();
}
public void transfer(int from,int to,double amount) throws InterruptedException {
lock.lock();
try{
while (accounts[from]<amount)
this.await();//不满足条件进入等待
System.out.print("当前线程:" + Thread.currentThread());
accounts[from] -= amount;
System.out.printf("%10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
this.signalAll();//唤醒等待线程
System.out.printf("总额:%10.2f ", getSum());
System.out.print(accounts[from]+" "+accounts[to]);
System.out.println();
}finally{
lock.unlock();
}
}
public double getSum(){
double sum=0;
for (double account : accounts) {
sum+=account;
}
return sum;
}
public int size(){return accounts.length;}
}
唤醒的线程可以去继续竞争资源,唤醒全部线程的原因是signal随机唤醒某一个线程,如果该线程不满足条件也会挂起,这样可能会导致死锁。
Synchronized关键字
还有一种方法是使用synchronized关键字,如果一个方法用synchronized声明,那么对象的锁会保护整个方法。
public Bank(int n,double initBalance){
accounts=new double[n];
Arrays.fill(accounts,initBalance);
//condition=lock.newCondition();
}
public synchronized void transfer(int from,int to,double amount) throws InterruptedException {
while (accounts[from]<amount)
this.wait();
System.out.print("当前线程:" + Thread.currentThread());
accounts[from] -= amount;
System.out.printf("%10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
this.notifyAll();
System.out.printf("总额:%10.2f ", getSum());
System.out.print(accounts[from]+" "+accounts[to]);
System.out.println();
}
public synchronized double getSum(){
double sum=0;
for (double account : accounts) {
sum+=account;
}
return sum;
}
public int size(){return accounts.length;}
}
可以看出这种方法十分简洁。
使用wait来将线程添加到等待集,notify/notifyAll唤醒随机一个/全部等待线程。