多线程
有时候,我们需要下载文件,使用单线程的时候只能由这个线程进行下载任务,不能完全发挥所有计算机资源,如果我们使用的是多线程的话,那么我们就可以把文件进行分隔,每个线程同时下载文件的一部分,这样就能充分发挥计算机资源,这是多线程中的一个典型应用了。
锁
某些特殊情况下,可能多个线程需要用到计算机的同一个资源,访问这些资源的代码段叫做临界区,这些资源叫做临界资源。比如我们生活中常遇到的存钱和取钱问题,账户里的钱是一个数字,我们不能同时对其进行取和存操作,否则会发生错误。在存的时候不允许进行取钱。取的时候 也不能存钱。这里就用到了锁。
package test;
public class Demo {
public int money = 10000;
public static void main(String[] Args) {
new Demo().test();
}
private void test() {
// TODO Auto-generated method stub
Thread t1 = new Thread(new drawmoney());
Thread t2 = new Thread(new savemoney());
t1.start();
t2.start();
}
class drawmoney implements Runnable {
@Override
public void run() {
while (true) {
money--;
System.out.println("取钱:" + money);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
// TODO Auto-generated method stub
}
}
class savemoney implements Runnable {
@Override
public void run() {
// TODO Auto-generated method stub
while (true) {
money++;
System.out.println("存钱:" + money);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
如同上面这个程序按照常理讲结果应该是取钱:9999,然后存钱10000这样交替进行。我们实际运行后会发现不是我们想的这样:结果如下
存钱:10000
取钱:9999
取钱:9999
存钱:10000
存钱:10001
完全乱了。两次取钱都是9999,第一次存钱 应该是10001,也不对
取钱:9999
取钱:9999
存钱:10000
存钱:10001
完全乱了。两次取钱都是9999,第一次存钱 应该是10001,也不对
这时候我们该用上传说中的锁了
package test;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo {
Lock drawmoney = new ReentrantLock();
public int money = 10000;
public static void main(String[] Args) {
new Demo().test();
}
private void test() {
// TODO Auto-generated method stub
Thread t1 = new Thread(new drawmoney());
Thread t2 = new Thread(new savemoney());
t1.start();
t2.start();
}
class drawmoney implements Runnable {
@Override
public void run() {
while (true) {
drawmoney.lock();
money--;
System.out.println("取钱:" + money);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
drawmoney.unlock();
}
}
}
class savemoney implements Runnable {
@Override
public void run() {
// TODO Auto-generated method stub
while (true) {
drawmoney.lock();
money++;
System.out.println("存钱:" + money);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
drawmoney.unlock();
}
}
}
}
再看一下加完锁后的运行结果:
取钱:9999
取钱:9998
取钱:9997
取钱:9996
存钱:9997
存钱:9998
存钱:9999
取钱:9998
取钱:9997
取钱:9996
存钱:9997
存钱:9998
存钱:9999
看上去虽然不是交替进行,但这结果确实是对的。这种情况可能是因为取钱的线程是先创建开始 运行的,并且时间片轮转的时候并不能做到平等运行。在这里java也为这种情况提供了解决办法,java中有一个带有公平策略的锁。我们只要去 Lock drawmoney = new ReentrantLock(true);这行代码中加入true这个参数就可以了。
运行结果:
取钱:9999
存钱:10000
取钱:9999
存钱:10000
取钱:9999
存钱:10000
存钱:10000
取钱:9999
存钱:10000
取钱:9999
存钱:10000
但是这种锁将大大降低性能,所以并不推荐使用。
死锁
如果我们在取钱这个线程里加一个条件,比如说只有钱大于10002时才能取钱 。这时当取钱获得了这个锁并开始进行操作的时候在这里卡住了,而存钱这个线程并没有获得锁又不能进行操作,这时死锁就发生了
class drawmoney implements Runnable {
@Override
public void run() {
while (true) {
drawmoney.lock();
if (money >= 10002) {
money--;
System.out.println("取钱:" + money);
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
drawmoney.unlock();
}
}
}
这里的解决办法是用Condition这个类。看名字就知道这是为了新的条件而创造的,当一个线程因为条件不满足而无法继续执行的时候,通过这个条件对象,可是先放弃当前锁,让它加入等待集中,直到能给它有机会来重新满足条件的线程来解除它的阻塞。
package test;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo {
Lock drawmoney = new ReentrantLock();
Condition changed = drawmoney.newCondition();
public int money = 10000;
public static void main(String[] Args) {
new Demo().test();
}
private void test() {
// TODO Auto-generated method stub
Thread t1 = new Thread(new drawmoney());
Thread t2 = new Thread(new savemoney());
t1.start();
t2.start();
}
class drawmoney implements Runnable {
@Override
public void run() {
while (true) {
drawmoney.lock();
while (money < 10005) {
try {
changed.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
money--;
System.out.println("取钱:" + money);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
drawmoney.unlock();
}
}
}
class savemoney implements Runnable {
@Override
public void run() {
// TODO Auto-generated method stub
while (true) {
drawmoney.lock();
money++;
System.out.println("存钱:" + money);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
changed.signalAll();
drawmoney.unlock();
}
}
}
}
需要注意的是,调用signalall()这个方法时,应该在获得锁对象之后 。还有一个sign()方法 ,这个方法是从等待集中随机选择一个线程来解除阻塞。而且它并不会立即激活这个线程,而是等待线程通过竞争实现对对象的访问。
上面这种加锁的方法显得有些笨重 ,其实java中也已经提供了更轻便的方法 。synchronized关键字来解决这个问题。不过如果我们知道了上述方法的话,那么对于synchronized的理解会更加容易。
对于刚才那种形式我们就可以写成如下程序
package test;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo {
public int money = 10000;
Lock drawmoney = new ReentrantLock();
public static void main(String[] Args) {
new Demo().test();
}
private void test() {
// TODO Auto-generated method stub
Thread t1 = new Thread(new drawmoney());
Thread t2 = new Thread(new savemoney());
t1.start();
t2.start();
}
class drawmoney implements Runnable {
@Override
public void run() {
while (true) {
synchronized (drawmoney) {
money--;
System.out.println("取钱:" + money);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
class savemoney implements Runnable {
@Override
public void run() {
// TODO Auto-generated method stub
while (true) {
synchronized (drawmoney) {
money++;
System.out.println("存钱:" + money);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
}
同时也允许用在方法上。
比如public synchronized void drawmoney()这样之后只允许一个线程来调用这个方法了。
读/写锁
声明方法 :
ReentrantReadWriteLock lock2 = new ReentrantReadWriteLock();
Lock readmoney = lock2.readLock();
Lock writemoney = lock2.writeLock();
用法和上面一样,需要注意的是。
读锁可以被多个读操作共用。但会排斥所有写操作。
写锁则和普通锁一样,排斥所有其它操作。