多线程的并发执行可以提高程序的效率,但是当多个线程去访问同一个资源时,也会引发一些安全问题。例如,当统计一个班的学生数目时,比如有同学进进出出,则很难被统计正确。解决这样的问题就需要实现多线程的同步,即限制某个资源在同一时刻只能被一个线程访问。
1.线程安全
class myThread implements Runnable{
private int tickets=10;
@Override
public void run() {
while(true){
if(tickets>0){
try{
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在发售第"+tickets--+"张票");
}
}
}
}
public class test {
public static void main(String[] args) {
myThread myThread = new myThread();
new Thread(myThread,"窗口1").start();
new Thread(myThread,"窗口2").start();
new Thread(myThread,"窗口3").start();
new Thread(myThread,"窗口4").start();
}
}
最后几行打印出来的结果有0和负数,这种现象不应该出现的,因为在程序中做了判断,只有当票号大于0时才会进行售票。运行结果中之所以出现了负数的票号是因为多线程在售票时出现了安全问题。
在售票程序的while循环中添加了sleep()方法,这样就模拟了售票过程中线程的延迟。由于线程有延迟,当票号减为1时,假设窗口2线程此时出售1号票,对票号进行判断后,进入while循环,在售票之前通过sleep()方法模拟售票时耗时操作,这时窗口1线程会进行售票,由于此时票号仍为1,因此窗口1线程也会进入循环,同理,四个线程都会进入while循环,休眠结束后,四个线程都会进行售票,这样就相当于将票号减了四次,结果中出现负号和0的情况。
2.代码同步块
想要解决线程安全的问题,必须要保证处理共享资源的代码在任意时刻只能有一个线程访问。 在Java中提供了线程同步机制。当多个线程使用同一个共享资源时,可以将处理共享资源的代码放置在一个使用synchronized关键字修饰的代码中,这段代码块被称为同步代码块。
synchronized (lock){
//操作共享资源块
}
在上述代码表示中, look是一个锁对象,它是同步代码块的关键。当线程执行同步代码块时,首先会检查锁对象的标志位,默认情况下标志位为1,此时线程会执行同步代码块,同时将锁对象的标志位置为0。当一个新的线程执行到这段同步代码块时,由于锁对象的标志位为0,新线程会发生阻塞,等待当前线程执行完同步代码块后,锁对象的标志位被置为1,新的线程才能进入同步代码块执行其中的代码。这样循环往复,直到共享资源被处理完为止。这个过程就像一个公用电话亭,只有当前面一个人打完电话出来后,后面的人才能进入进行打电话。
class myThread implements Runnable{
private int tickets=10;
Object lock=new Object();
@Override
public void run() {
while(true){
synchronized (lock){
if(tickets>0){
try{
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在发售第"+tickets--+"张票");
}
}
}
}
}
public class test {
public static void main(String[] args) {
myThread myThread = new myThread();
new Thread(myThread,"窗口1").start();
new Thread(myThread,"窗口2").start();
new Thread(myThread,"窗口3").start();
new Thread(myThread,"窗口4").start();
}
}
注: 同步代码块中的锁对象可以是任何类型的对象,但多个线程共享的锁对象必须是相同的。“任意”说的是共享锁对象的类型,所以,所对象的创建代码不能放到run()方法中,否则每个线程运行到run()方法都会创建一个新的对象,这样每个线程都会有一个不同的锁,每个锁都有自己的标志位,线程之间就不能产生同步的效果。
3.同步方法
同步代码块可以有效的解决线程的安全问题,当把共享资源的操作放在synchronized 定义的区域内时,便为这些操作加了同步锁。
同样,在方法面前也可以使用synchronized关键字来修饰,被修饰的方法为同步方法,他能实现和同步代码块同样的功能,语法如下:
class myThread implements Runnable{
private int tickets=10;
Object lock=new Object();
@Override
public void run() {
while(true){
saleTicket();
}
}
private synchronized void saleTicket(){
if(tickets>0){
try{
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在发售第"+tickets--+"张票");
}
}
}
public class test {
public static void main(String[] args) {
myThread myThread = new myThread();
new Thread(myThread,"窗口1").start();
new Thread(myThread,"窗口2").start();
new Thread(myThread,"窗口3").start();
new Thread(myThread,"窗口4").start();
}
}
注:
(1)同步方法也有自己的锁,它的锁就是当前调用该方法的对象,也就是this所指的对象。这样做的好处是,同步方法被所有线程所共享,方法所在的对象相当于所有线程来说是唯一的,从而保证了锁的唯一性。当一个线程执行该方法时,其他的线程就不能进入该方法中,直到这个线程执行完该方法为止,从而达到了线程同步的效果。
(2)有时候线程同步的方法是静态方法,今天方法不需要创建对象就就可以直接用 类名.方法名()的方式调用。如果不创建对象,静态同步方法的锁就不会是this, Java中静态方法的锁是该方法所在类的class对象。
(3)同步解决了多个线程同时访问共享数据时的线程安全问题,只要加上同一个锁,在同一时间内只有一条线程执行,但是在线程执行同步代码块时每次都会判断锁的状态,非常消耗资源,效率极低。
4.同步锁
synchronized同步代码块和同步方法使用的是一种封闭式的锁机制,使用非常简单,也能够解决线程同步过程中出现的线程安全问题,但是也有一些限制,例如它无法中断一个正在等候获得锁的线程,也无法通过轮询得到锁。如果不想等下去,也就没办法得到锁。
从JDK 5开始,Java增加了一个功能强大的Lock锁。Lock锁与synchronized隐式锁在功能上基本相同,Lock锁最大的优势是可以让某个线程在持续获取同步锁失败后返回,不在继续等待,另外Lock锁在使用时更加的灵活。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class myThread implements Runnable{
private int tickets=10;
private final Lock lock=new ReentrantLock();
@Override
public void run() {
while(true){
lock.lock(); //对代码块进行加锁
if(tickets>0){
try{
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"正在发售第"+tickets--+"张票");
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock(); //执行完代码块后释放锁
}
}
}
}
}
public class test {
public static void main(String[] args) {
myThread myThread = new myThread();
new Thread(myThread,"窗口1").start();
new Thread(myThread,"窗口2").start();
new Thread(myThread,"窗口3").start();
new Thread(myThread,"窗口4").start();
}
}
注: ReentrantLock类是Lock锁接口的实现类,也是常用的同步锁,在该同步锁中除了lock()方法和unlock()方法之外,还有一些其他的同步锁操作的方法,例如trylock()方法可以判断某个线程锁是否可用。另外,在使用Lock同步锁时,可以根据需要在不同的代码位置灵活的上锁和解锁,为了保证所有情况下都能正常解锁以保证其他线程可以执行,通常在finally()代码块中调用unlock()方法来解锁。。
5.死锁
线程A持有锁L并想获得锁M的同时,线程B持有锁M并尝试获得锁L,那么这两个线程将永远地等待下去,这种情况就是死锁形式(或者称为"抱死").