Java 多线程/并发 Synchronized学习笔记

本文深入解析Java中的synchronized关键字,探讨其在方法和代码块上的应用,以及如何处理锁的可重入性、死锁、异常释放等问题。通过实例演示不同场景下synchronized的行为表现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

synchronized关键字可以添加在方法的声明上,也可以添加在代码块中
添加在方法上时分两种情况,当为静态方法时,表示的是对该类的.class对象上锁
当不为静态方法时,表示的是对该类的对象上锁。
添加在代码块时,需要指定上锁的对象。

public class Synchonizedd {
    static Long start,end;
    static
    {
        start = System.currentTimeMillis();
    }
    //锁定this对象
    public synchronized  void m1()  {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        end =System.currentTimeMillis();
        System.out.println(end-start+" m1");
    }
    //锁定Synchonizedd.class 对象
    public static synchronized void m2(){
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        end =System.currentTimeMillis();
        System.out.println(end-start+" m2");
    }
    public void m3()  {
    	//锁定this对象
        synchronized (this)
        {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            end =System.currentTimeMillis();
            System.out.println(end-start+" m3");
        }
    }

    public static void main(String[] args) {
        Synchonizedd synchonizedd = new Synchonizedd();
        new Thread(()->synchonizedd.m1()).start();
        new Thread(()->Synchonizedd.m2()).start();
        new Thread(()->synchonizedd.m3()).start();
    }
}

运行结果:
在这里插入图片描述
可以看到,因为m1和m2方法锁定的不是同一对象,所以调用m1方法的线程和调用m2方法的线程能够并行执行。
而m1和m3中的synchronized代码块锁定的是同一对象,调用两个方法的线程不能并行运行,等到其中一个释放该对象锁之后另一个线程才会运行。
synchnized 操作是原子操作,不可分。
一个synchnized 方法运行中,一个非同步方法是可以运行的

public class T2 {
    static long start,end;
    static
    {
        start = System.currentTimeMillis();
    }
    public synchronized  void m1()
    {
        System.out.println(Thread.currentThread().getName()+" m1");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        end=System.currentTimeMillis();
        System.out.println("当前时间为:"+(end-start) +" m1 to end");
    }
    public  void m2()
    {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        end=System.currentTimeMillis();
        System.out.println("当前时间为:"+(end-start)+" "+Thread.currentThread().getName()+" m2");
    }

    public static void main(String[] args) {
        T2 t2 = new T2();
        new Thread(()->t2.m1(),"Thread1").start();
        new Thread(()->t2.m2(),"Thread2").start();
    }
}

运行结果:
在这里插入图片描述
上述代码中,我们开启了两个线程,一个线程执行m1方法,另一个执行m2方法,m1方法执行是需要获得锁的,m2方法不需要获得锁的,所以当m1执行的过程中,m2方法是可以同时执行的。
一个同步方法可以调用另一个同步方法,一个线程已经拥有某个对象的锁,再次申请时仍然会得到该对象的锁,也就是说synchronized获得的锁是可重入的。

public class T3 {
    public synchronized void m1()
    {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //执行同一个需要获得锁的方法
        m2();
        System.out.println(Thread.currentThread().getName()+" m1");
    }
    public synchronized  void m2()
    {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+" m2");
    }

    public static void main(String[] args) {
        T3 t3 = new T3();
        new Thread(()->t3.m1(),"th1").start();
    }
}

运行结果:
在这里插入图片描述
上述代码中,我们开启了一个线程,这个线程去执行m1方法,m1方法是需要获得锁的,m1方法中又需要调用m2方法,而m2方法也需要获得锁,这就形成了需要获得T3类对象的两次锁,而实验结果表明m2方法是可以执行的,这说明synchronized获得的锁是可重入的。
子类调用父类的同步方法,锁定的是同一对象

public class T4 {
    synchronized void m()
    {
        System.out.println("m start");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("锁定的对象是 "+this);
        System.out.println("m end");
    }

    public static void main(String[] args) {
        TT4 tt4 = new TT4();
        new Thread(()->tt4.m(),"Thread").start();
    }
}
class TT4 extends T4
{
    public synchronized void m()
    {
        System.out.println("child m start");
        super.m();
        System.out.println("锁定的对象是 "+this);
        System.out.println("child m end");
    }
}

运行结果:
在这里插入图片描述
通过运行结果发现,锁定的确实是同一对象。
程序在执行过程中,如果发现异常,默认情况下锁是会被释放。
所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。
比如,在一个web app处理过程中,多个servlet线程共同访问一个资源,这时如果异常处理不合适,在一个线程中抛出异常,其他线程就会进入同步代码区,有可能访问到异常产生时的数据。
因此要非常小心的处理同步业务逻辑中的异常。

public class T5 {
    int count = 0;
    public synchronized void m()
    {
        while (true) {
            count++;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (count == 5) {
                //加上try catch块则不会释放锁
                /*try {
                    int i = 1 / 0;
                }catch (Exception e)
                {
                    e.printStackTrace();
                }*/
                //抛出异常,并释放锁
                int i = 1 / 0;
            }
            System.out.println(Thread.currentThread().getName()+" count="+count);
        }
    }

    public static void main(String[] args) {
        T5  t5 = new T5();
        new Thread(()->t5.m(),"t1").start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->t5.m(),"t2").start();
    }
}

当不对异常进行处理时的运行结果(这里截取出现异常后的运行结果):
在这里插入图片描述
当对异常进行trycatch处理时的运行结果:
在这里插入图片描述
上述代码中,当抛出异常时,正在执行的方法会释放锁,而用trycatch处理之后,正在执行的方法不会释放锁。
synchronize 优化
同步代码块中的语句越少越好

public class T10 {
    int count = 0;
    synchronized void m1()
    {
        try {
            Thread.sleep(2000);
            for (int i=0;i<1000000;i++) count++;
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
    void m2()
    {
        try {
            Thread.sleep(2000);
            //业务逻辑中只有这句话需要sync,这时不应该给整个方法上锁
            //采用细粒度的锁,可以使线程争用时间变短,从而提高效率
            synchronized (this)
            {
                for (int i=0;i<1000000;i++) count++;

            }
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

锁定某个对象o,如果o的属性发生改变,不影响锁的使用。
但是如果o引用另外一个对象,则锁定的对象改变。
应避免将锁定对象的引用变成另外的对象

public class T11 {
    Object o = new Object();

    void m1()
    {
        // 锁定o引用的对象
        synchronized (o) {
            while (true) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("运行的线程:"+Thread.currentThread().getName() );
            }
        }
    }

    public static void main(String[] args) {
        T11 t11 = new T11();
        new Thread(()->t11.m1(),"t1").start();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Thread t2= new Thread(()->t11.m1(),"t2");
        // 改变引用对象
        t11.o  = new Object();
        t2.start();
    }
}

运行结果:
在这里插入图片描述
上述代码中,先开启线程t1,然后t1执行m1方法,之后改变o的引用,然后开启一个线程t2,可以看到t1和t2同时运行,这是因为t2和t1获得的锁的对象是不一样的。
就像下面图片一样:
在这里插入图片描述
不要以字符串常量作为锁定对象。
在下面的例子中,m1和m2其实锁定的是同一个对象。
这种情况还会发生比较隐退的现象,比如你用到一个类库,在该类库中代码锁定了字符串"Hello"。
但是你读不到源码,所以在自己的代码中也锁定了"Hello",这时候就有可能发生非常诡异的死锁阻塞。
因为你的程序和你用到的类库不经意间使用了同一把锁

public class T12 {
    String s1 = "Hello";
    String s2 = "Hello";
     void m1()
    {
        synchronized (s1)
        {
            while(true)
            {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("运行的线程:"+Thread.currentThread().getName());
            }
        }
    }
    void m2()
    {
        synchronized (s2)
        {
            while(true)
            {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("运行的线程:"+Thread.currentThread().getName());
            }
        }
    }

    public static void main(String[] args) {
        T12 t12 = new T12();
        new Thread(()->t12.m1(),"t1").start();
        new Thread(()->t12.m2(),"t2").start();
    }
}

运行结果:
在这里插入图片描述
使用Synchronized实现死锁
死锁形成的原理就是线程A需要线程B所占用的锁来执行程序,而线程B同时也需要线程A锁占用的锁来执行程序,这就导致了两个线程无休止的等待,等待对方释放占用的锁。

public class DeadLock {

    public static void main(String[] args) {
        D1 d1 = new D1();
        D2 d2 = new D2();
        new Thread(()->d1.m1(d2),"Thread1").start();
        new Thread(()->d2.m1(d1),"Thread2").start();
    }
}
class D1
{

    public synchronized void  m1(D2 d2)
    {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName());
        d2.m2();
    }
    public synchronized void m2()
    {
        System.out.println("D1 m2");
    }
}
class D2
{

    public synchronized void m1(D1 d1)
    {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName());
        d1.m2();
    }
    public synchronized  void m2()
    {
        System.out.println("D2 m2");
    }
}

运行结果:
在这里插入图片描述
可见线程1和线程2都在等待对方释放锁,而自己需要等待对方释放锁之后才释放自己的锁,这就导致了死锁的产生。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值