Java线程的安全问题

本文最后更新于 2024年11月19日

当多个线程同时访问同一资源(变量,文件,记录),如果只有读操作,则不会有线程安全问题,如果有读和写操作,则会产生线程安全问题,必须保证共享数据同一时刻只能有同一个线程操作。Java采取的办法是synchronized同步代码块或同步方法。同步代码块或同步方法解决了线程安全问题,但是操作共享数据时,线程时串行执行的,意味着效率较低。

1.多线程安全问题

经典卖票案例:

两个线程一块卖票,没有加同步代码块,程序运行结果不正确,存在超卖重卖


public class Ticket {

    public static void main(String[] args) {

        TicketTask t1 = new TicketTask();
        TicketTask t2 = new TicketTask();

        t1.start();
        t2.start();

    }

    static class TicketTask extends Thread {

        static int ticket = 200;

        @Override
        public void run() {
            while (true) {

                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() +" " + ticket);
                    ticket--;
                }
                else {
                    break;
                }
            }
        }
    }
}

2 同步代码块和同步方法解决线程安全问题

2.1 同步代码块

需要被同步的代码,即为操作共享数据的代码。共享数据,即为多个线程都需要操作的数据。同步监视器可以由任何类的对象担任,但是多个线程必须共用同一个同步监视器。

同步代码块的语法

synchronized (同步监视器/) {
  //需要被同步的代码
}

加入同步代码块,程序运行结果正确,当有线程操作共享数据,其他线程需要等待。lock作为同步监视器,锁住代码块中的操作,谁获得同步监视器,谁运行同步代码块中的代码。


public class Ticket {

    public static void main(String[] args) {

        TicketTask t1 = new TicketTask();
        TicketTask t2 = new TicketTask();

        t1.start();
        t2.start();

    }

    static class TicketTask extends Thread {

        static int ticket = 200;
        static final Object lock = new Object();

        @Override
        public void run() {
            while (true) {

                synchronized (lock) {

                    if (ticket > 0) {
                        System.out.println(Thread.currentThread().getName() +" " + ticket);
                        ticket--;
                    }
                    else {
                        break;
                    }

                }

            }
        }
    }
}

2.2 同步方法

如果需要同步执行的代码恰好在一个方法中,可以使用同步方法保证线程安全,在方法声明上使用 synchronized 关键字,此时锁为对象实例(this)。同一时刻,只有一个线程能够执行该实例的方法。

public synchronized void test() {

}

3 同步代码块和同步方法的使用

3.1 Runnable创建线程时使用同步代码块

使用synchronized使得实例方法加锁变为同步方法,因为Runnable对象只有一个,所以锁可以直接使用当前调用者this


public class TestRunnable {
    public static void main(String[] args) {
        Target target = new Target();
        for (int i = 0; i < 10; i++) {
            new Thread(target, "T"+i).start();
        }

    }

}


class Target implements Runnable {

    private Integer i = 1000;

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (this) {
                if (i > 0) {
                    i--;
                    System.out.println(Thread.currentThread().getName() + "->" + i);
                } else {
                    break;
                }
            }

        }

    }

}

3.2 Thread类创建线程时,使用同步代码块

Thread类创建线程时,使用同步代码块加锁,因为Thread对象是多个,所以需要静态的监视器对象object,如果还用this就出现了多个锁

public class TestThread {

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new MyThread("T"+i).start();
        }
    }

}



class MyThread extends Thread {

    private static Integer i = 1000;
    //同步监视器,锁
    private static Object object = new Object();

    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        while(true) {
 
            synchronized(object) {
                if (i > 0) {
                    i--;
                    System.out.println(Thread.currentThread().getName() +"->" + i);
                } else {
                    break;
                }
            }
        }
    }
}

3.3 Runnable创建线程时,使用同步方法加锁

public class TestRunnable {
    public static void main(String[] args) {

        Runnable runnable = new Runnable() {

            private int num = 1000;

            @Override
            public void run() {
                while (true) {
                    this.show();
                }
            }
 
            public synchronized void show() {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                if (num > 0) {
                    num--;
                    System.out.println(Thread.currentThread().getName() + "->" + num);
                }
            }

        };

        for (int i = 0; i < 10; i++) {
            new Thread(runnable, "T" + i).start();
        }

    }
}

3.4 Thread创建线程时,使用同步方法加锁

public class TestThread {

    public static void main(String[] args) {

        TicketTest ticketTest = new TicketTest();

        for (int i = 0; i < 10; i++) {
            new Thread("T"+i) {

                @Override
                public void run() {
                    while (true) {
                        ticketTest.test();
                    }
                }

            }.start();
        }
    }

    static class TicketTest  {

        private int num = 1000;

        private synchronized void test() {

            if (num > 0) {
                num--;
                System.out.println(Thread.currentThread().getName() + "->" + num);
            }
        }

    }

}

3.5 静态方法加锁和使用.class对象做锁

在静态方法上使用 synchronized,锁住的是类的.class对象,每个类的class对象只有一个,所以同时只能有一个线程进入方法。

public static synchronized void staticMethod() {
    // 方法体
}

同步块上使用.class对象做锁,因为每个类.class对象只有一个,故也能用于保证线程安全

synchronized (A.class) {

}

4 死锁

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了死锁。一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。

写程序时,要避免出现死锁。

示例代码1

死锁原因的分析:这个例子是个比较明显的死锁,线程t1t2几乎同时启动,在一秒钟的等待时间里,t1获得了锁lock1t2获得了锁lock2,一秒钟后t1又想去获得lock2,但是现在lock2t2持有,需要一直等直到t2释放lock2,与此同时,t2也想去获得lock1,但是lock1现在被t1持有,需要一直等待,直到t1释放lock1,两个线程都在争抢在对方持有的锁,且都在等待对方先释放各自持有的锁,不然就一直等待,线程都一直处在阻塞状态无法继续运行,造成死锁。


public class TestDeadLock {
    public static void main(String[] args) {
        Object lock1 = new Object();
        Object lock2 = new Object();

        Thread t1 = new Thread() {
            @Override
            public void run() {
                synchronized (lock1) {

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }

                    synchronized (lock2) {
                        System.out.println(lock1);
                        System.out.println(lock2);
                    }
                }

            }
        };


        Thread t2 = new Thread(){
            @Override
            public void run() {
                synchronized (lock2) {

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }

                    synchronized (lock1) {
                        System.out.println(lock1);
                        System.out.println(lock2);
                    }
                }
            }
        };


        t1.start();
        t2.start();


    }
}

示例代码2

这是一个不是非常明显的死锁的例子,线程thread1thread2几乎同时开始执行,thread1执行a.fun(b)时,由于A类的fun方法是个同步方法,故锁是当前调用者this对象,即a,调用fun方法,thread1便持有了锁a,与此同时,thread2同理的持有了锁b,这些都在一秒钟前完成了,1秒钟后,thread1执行blast同步方法,同理需要先获得锁b,但是锁b目前被thread2持有,同时thread2也开始执行alast方法,需要先持有锁a,但是锁athread1持有,双方都在等待对方先释放自己需要的锁,否则就一直阻塞无法继续运行,造成死锁。

public class TestDeadLock2  {

    public static void main(String[] args) {

        A a = new A();
        B b = new B();

        Thread thread1 = new Thread() {
            @Override
            public void run() {
                a.fun(b);
            }
        };


        Thread thread2 = new Thread() {
            @Override
            public void run() {
                b.fun(a);
            }
        };

        thread1.start();
        thread2.start();

    }



    public static class A extends Thread {

        public synchronized void fun(B b) {


            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            b.last();
        }

        public synchronized void last() {

        }

    }

    public static class B extends Thread {

        public synchronized void fun(A a) {

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            a.last();
        }

        public synchronized void last() {
        }

    }

}

Java线程的安全问题
https://blog.liuzijian.com/post/B9284A6F-30FD-F130-B225-517EC7D41646.html
作者
Liu Zijian
发布于
2022年5月1日
更新于
2024年11月19日
许可协议