一、为什么要用synchronized关键字

首先多线程中多个线程运行面临共享数据同步的问题。

多线程正常使用共享数据时需要经过以下步骤:

1.线程A从共享数据区中复制出数据副本,然后处理。

2.线程A将处理好的数据副本写入共享数据区。

3.线程B从共享数据区中复制出数据副本。

如此循环,直到线程结束。

假如线程A从共享数据区中复制出数据副本然后处理,在还没有将更新的数据放入主内存时,线程B来到主内存读取了未更新的数据,这样就出问题了。

这就是所谓的脏读,这类问题称为多线程的并发问题。

举个具体的例子:

 public class TestThread {
public static void main(String[] args){
TestSynchronized s = new TestSynchronized();
new Thread(s,"t1").start(); //两个线程访问一个对象
new Thread(s,"t2").start();
}
} class TestSynchronized implements Runnable{
private int ticket = 5; public void run(){
for(int p = 0; p < 10; p++){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(ticket >= 0){
System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
}
}
}
}
运行结果:
t2 ticket:4
t1 ticket:5
t1 ticket:2
t2 ticket:3
t1 ticket:1
t2 ticket:1
t2 ticket:0

可以看到1号票同时给了t1和t2,当t1读入1执行了ticket--后,数据还没有来得及写入主内存就被t2从主内存中读走了1,就造成了这种现象。

要想避免这种现象就需要使用synchronized关键字,synchronized英译为同步,我们认为暂且把他看做锁定更好理解。

接下来我们看看synchronized如何使用。

二、synchronized的用法

1. synchronized修饰方法(也称同步方法)

(1) java中每个对象都有一个锁(lock),或者叫做监视器,当前线程访问某个对象中synchronized修饰的方法(同步块)时,线程需要获取到该对象的锁,获取对象锁后才能访问该对象中synchronized方法(同步块),且一个对象中只有一个锁。

(2) 没有获得该对象的锁的其他线程,无法访问该对象中synchronized修饰的方法(同步块)。

(3) 其他线程要想访问该对象中synchronized修饰的方法需要获取该对象的锁。

(4) 对象锁只有将synchronized方法(同步块)中的内容运行完毕或遇到异常才会释放锁。

例一:

 public class TestThread {
public static void main(String[] args){
TestSynchronized s = new TestSynchronized();
new Thread(s,"t1").start(); //两个线程访问一个对象
new Thread(s,"t2").start();
}
} class TestSynchronized implements Runnable{
private int ticket = 5; synchronized public void run(){
for(int p = 0; p < 10; p++){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(ticket >= 0){
System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
}
}
}
}
运行结果:
t1 ticket:5
t1 ticket:4
t1 ticket:3
t1 ticket:2
t1 ticket:1
t1 ticket:0

我们来分析上面程序,首先线程t1进去run方法获得对象s的锁,然后执行完run方法释放锁,run运行忘了也就没有t2的事了。

因为只有将synchronized修饰的方法执行完才会释放锁,故打印五个t1.。

还有一点,如果一个对象里面有多个synchronized方法,某一时刻只能有一个线程进入其中一个synchronized修饰的方法,则这时其他任何线程无法进入该对象中任何一个synchronized修饰的方法。

补充片段:

 public class TestThread {
public static void main(String[] args){
Test m1 = new Test(); //两个线程共访问一个对象。 TestSynchronized_1 s1 = new TestSynchronized_1(m1);
TestSynchronized_2 s2 = new TestSynchronized_2(m1);
new Thread(s1,"t1").start();
new Thread(s2,"t2").start();
}
} class Test{
synchronized public void test1(){
for(int p = 0; p < 5; p++){
System.out.println("s1.run.TestSynchronized_test 1");
}
} synchronized public void test2(){
for(int p = 0; p < 5; p++){
System.out.println("s2.run.TestSynchronized_test 2");
}
}
} class TestSynchronized_1 implements Runnable{ private Test m;
public TestSynchronized_1(Test m){
this.m = m;
} public void run(){
m.test1();
}
} class TestSynchronized_2 implements Runnable{ private Test m;
public TestSynchronized_2(Test m){
this.m = m;
} public void run(){
m.test2();
}
}
运行结果:
s1.run.TestSynchronized_test 1
s1.run.TestSynchronized_test 1
s1.run.TestSynchronized_test 1
s1.run.TestSynchronized_test 1
s1.run.TestSynchronized_test 1
s2.run.TestSynchronized_test 2
s2.run.TestSynchronized_test 2
s2.run.TestSynchronized_test 2
s2.run.TestSynchronized_test 2
s2.run.TestSynchronized_test 2

当线程t1运行synchronized修饰的test1方法时,线程t2是无法运行test2方法。结合之前说的,一个对象锁只有一把,而这里是两个线程共享对象(m1),当线程t1获得锁时,线程t2就只能等待。归根结底把握几个要点:

1.锁的唯一性(一个对象只有一把锁,但不同对象就有不同的锁)

2.没锁不能进去入synchronized修饰内容中运行。

3.只有运行完synchronized修饰的内容或遇到异常才释放锁。

我们来看下面这个代码:

例二:

 public class TestThread {
public static void main(String[] args){
Mouth m1 = new Mouth();
Mouth m2 = new Mouth();
TestSynchronized s1 = new TestSynchronized(m1);//两个线程访问两个对象。
TestSynchronized s2 = new TestSynchronized(m2);
new Thread(s1,"t1").start(); //线程t1
new Thread(s2,"t2").start(); //线程t2
}
} class Mouth{ //资源及方法
synchronized public void test(){
int ticket = 5;
for(int p = 0; p < 10; p++){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(ticket >= 0){
System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
}
}
} } class TestSynchronized implements Runnable{
private Mouth m = new Mouth(); public TestSynchronized(Mouth m){
this.m = m;
} synchronized public void run(){
m.test();
}
}
运行结果:
t1 ticket:5
t2 ticket:5
t2 ticket:4
t1 ticket:4
t1 ticket:3
t2 ticket:3
t2 ticket:2
t1 ticket:2
t1 ticket:1
t2 ticket:1
t2 ticket:0
t1 ticket:0

可以发现好像用synchronized修饰的test方法没有起作用,怎么是t1,t2怎么是交替运行的?

我们回顾下之前说的对象锁,线程获得对象锁后可以访问该对象里面synchronized修饰的方法,其他线程无法访问。

我们上面的代码里面对象有两个,一个是m1、一个是m2。

t1获得了对象m1的锁,然后访问m1中的test方法;t2获得了对象m2的锁,然后访问s2中的test方法。

线程t1和线程t2访问的是不同的资源(m1,m2),并不相互干扰所以没有影响。例一中是因为两个线程访问同一个资源(s1)所以synchronized的起了限制作用。

synchronized修饰方法时只能对多个线程访问同一资源(对象)时起限制作用。

可能大家会说了,那我们有没有办法也限制下这种情况呢,答案当然是可以的。

这就是下面要说的:

2.synchronized修饰静态方法

当修饰静态方法时锁定的是,而不是对象,我们先把例二修改下看下结果。

例三:

 public class TestThread {
public static void main(String[] args){
Mouth m1 = new Mouth();
Mouth m2 = new Mouth();
TestSynchronized s1 = new TestSynchronized(m1);
TestSynchronized s2 = new TestSynchronized(m2); //两个线程访问两个对象
new Thread(s1,"t1").start();
new Thread(s2,"t2").start();
}
} class Mouth{
synchronized public static void test(){ //改为静态方法,锁定的是类。
int ticket = 5;
for(int p = 0; p < 10; p++){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(ticket >= 0){
System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
}
}
} } class TestSynchronized implements Runnable{
private Mouth m = new Mouth(); public TestSynchronized(Mouth m){
this.m = m;
} synchronized public void run(){
m.test();
}
}
运行结果:
t1 ticket:5
t1 ticket:4
t1 ticket:3
t1 ticket:2
t1 ticket:1
t1 ticket:0
t2 ticket:5
t2 ticket:4
t2 ticket:3
t2 ticket:2
t2 ticket:1
t2 ticket:0

当synchronized修饰静态方法时,线程需要获得类(Mouth)锁才能运行,没有获得类锁的线程无法运行,且获得类锁的线程会将synchronized修饰的静态方法会运行完毕才释放类锁。

例如例三中的代码,t1先获得类(Mouth)锁运行Mouth类中的test方法,而t2没有类(Mouth)锁就无法运行。一个类只有一个类锁,却可以有多个对象(t1,t2等...)都是一个类(Mouth)中的对象,只要一个线程获取了类(Mouth)锁,其他线程就要等到类锁被释放,然后获得类(Mouth)锁之后才能运行类(Mouth)中synchronized修饰的静态方法。所以即使是两个线程(t1,t2)访问两个不同的资源(m1,m2)也会受到限制,因为m1,m2都属于一个类(Mouth),而锁住类(Mouth)后每次只能有一个线程访问该类(Mouth)中的sychronized修饰的静态方法。

当t1访问m1中的test时,首先获得类(Mouth)锁,这时如果t2访问m2中的test方法时也需要获得类锁,可是这时类锁已经被线程t1获得,故t2无法访问m2中的方法。只有等t1运行完方法中的内容或异常释放锁后t2才有机会获得锁,获得锁后才能运行。

而之前例一中t1,t2锁的是对象,需要结合这几段代码理解下。

3.synchronized块(也称同步块)

如果每次都锁定的范围都是一个方法,每次只能有一个线程进去势必会导致效率的低下,这主要是锁定范围过多引起的。

这时可以根据实际情况锁定合适的区域,这就要用到同步块了

synchronized(需要锁住的对象或类){

       锁定的部分,需要锁才能运行。
}

()中可以确定锁定的是对象还是类,锁定对象的话可以用this,对类上锁类名加class,例如要锁定Mounth类(Moutn.class)。

我们首先看个没有任何同步的例子:

 public class TestThread {
public static void main(String[] args){
TestSynchronized s1 = new TestSynchronized(); new Thread(s1,"t1").start(); //两个线程访问一个对象
new Thread(s1,"t2").start();
}
} class TestSynchronized implements Runnable{
private int ticket = 5; public void run(){
for(int p = 0; p < 10; p++){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(ticket >= 0){
System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
}
}
}
}
运行结果:
t1 ticket:5
t2 ticket:4
t2 ticket:3
t1 ticket:2
t2 ticket:1
t1 ticket:1
t2 ticket:0
t1 ticket:-1

其中出现了-1,我们在其中一块区域加上同步形成同步块。

 public class TestThread {
public static void main(String[] args){
TestSynchronized s1 = new TestSynchronized(); //两个线程访问一个对象 new Thread(s1,"t1").start();
new Thread(s1,"t2").start();
}
} class TestSynchronized implements Runnable{
private int ticket = 5; public void run(){
for(int p = 0; p < 10; p++){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized(this){ //此次加上同步块,这部分内容一次只有一个线程可以进入,其他内容不受约束。
if(ticket >= 0){ //这里锁的是对象,这里面的内容需要对象锁才能运行。
System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
}
}
}
}
}
运行结果:
t2 ticket:5
t1 ticket:4
t1 ticket:3
t2 ticket:2
t1 ticket:1
t2 ticket:0

一次只能有一个线程进入同步块中,就不会出现线程读了未更新的数据或者多减一次的情况。未加synchronized修饰的其他区域不受影响,故两个线程的顺序不定。

下面我们来看一个同步块锁定的例子,效果和例三一样,只不过例三使用静态方法锁定了类,而下面这个是使用同步块锁定了类。

 public class TestThread {
public static void main(String[] args){
TestSynchronized s1 = new TestSynchronized();
TestSynchronized s2 = new TestSynchronized(); //两个线程访问两个对象
new Thread(s1,"t1").start();
new Thread(s2,"t2").start();
}
} class TestSynchronized implements Runnable{
private int ticket = 5; synchronized public void run(){
synchronized(TestSynchronized.class){ //将synchronized修饰的静态方法改成了同步块。 for(int p = 0; p < 10; p++){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(ticket >= 0){
System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
}
}
}
}
}
运行结果:
t1 ticket:5
t1 ticket:4
t1 ticket:3
t1 ticket:2
t1 ticket:1
t1 ticket:0
t2 ticket:5
t2 ticket:4
t2 ticket:3
t2 ticket:2
t2 ticket:1
t2 ticket:0

上述代码和例三功能一样,只是锁定方法不同,这里只是做下演示。

synchronized修饰方法是一种粗颗粒的并发控制,某一时刻只有一个线程执行方法内的内容效率较低下。

synchronized同步块是一种细颗粒的并发控制,可以自行根据需求确定区域较为灵活,可以平衡下效率和安全,同时也能因选择区域不恰当而造成问题。

只要不在synchronized方法(同步块)内的其他部分都不受限制。

普通方法锁定的对象,需要获得对象锁

静态方法锁定的是类,需要获得类锁。

同步块可以确定是锁对象(this )还是锁类(xxx.class),同时也可以自行确定区域。

2.3多线程(java学习笔记)synchronized关键字的更多相关文章

  1. Java学习笔记18---final关键字修饰变量、方法及类

    英语里final这个单词大家都知道是"最终的"意思,其实还有一个意思是"不可更改的".在Java里,final关键字作"不可更改的"来解释更 ...

  2. 2.1多线程(java学习笔记) java中多线程的实现(附静态代理模式)

    一.多线程 首先我们要清楚程序.进程.线程的关系. 首先进程从属于程序,线程从属于进程. 程序指计算机执行操作或任务的指令集合,是一个静态的概念. 但我们实际运行程序时,并发程序因为相互制约,具有“执 ...

  3. Java学习笔记——static关键字与静态的使用方法

    static:可以修饰成员变量和成员方法. 当变量被static修饰后,则其可以直接被类名调用.类名.成员. static特点: 随着类的加载而加载: 优先于对象存在: 被所有的对象共享,节省空间,但 ...

  4. java学习笔记11-static关键字

    如果在类中使用static关键字创建方法,这种方法称为类方法,可以在这个类中直接引用.而不是用static创建的方法.这种方法称为对象方法(实例方法),需要创建对象后才能使用. package les ...

  5. 0037 Java学习笔记-多线程-同步代码块、同步方法、同步锁

    什么是同步 在上一篇0036 Java学习笔记-多线程-创建线程的三种方式示例代码中,实现Runnable创建多条线程,输出中的结果中会有错误,比如一张票卖了两次,有的票没卖的情况,因为线程对象被多条 ...

  6. Java多线程技术学习笔记(二)

    目录: 线程间的通信示例 等待唤醒机制 等待唤醒机制的优化 线程间通信经典问题:多生产者多消费者问题 多生产多消费问题的解决 JDK1.5之后的新加锁方式 多生产多消费问题的新解决办法 sleep和w ...

  7. Java学习笔记-多线程-创建线程的方式

    创建线程 创建线程的方式: 继承java.lang.Thread 实现java.lang.Runnable接口 所有的线程对象都是Thead及其子类的实例 每个线程完成一定的任务,其实就是一段顺序执行 ...

  8. [java学习笔记]java语言核心----面向对象之this关键字

    一.this关键字 体现:当成员变量和函数的局部变量重名时,可以使用this关键字来区别:在构造函数中调用其它构造函数 原理:         代表的是当前对象.         this就是所在函数 ...

  9. java学习笔记14--多线程编程基础1

    本文地址:http://www.cnblogs.com/archimedes/p/java-study-note14.html,转载请注明源地址. 多线程编程基础 多进程 一个独立程序的每一次运行称为 ...

  10. 《Java学习笔记(第8版)》学习指导

    <Java学习笔记(第8版)>学习指导 目录 图书简况 学习指导 第一章 Java平台概论 第二章 从JDK到IDE 第三章 基础语法 第四章 认识对象 第五章 对象封装 第六章 继承与多 ...

随机推荐

  1. Java 冒泡排序与快速排序的实现

    冒泡排序   基本特点 (1)基于交换思想的排序算法  (2)从一端开始,逐个比较相邻的两个元素,发现倒序即交换.      (3)一次遍历,一定能将其中最大(小)的元素交换到其最终位置上 排序过程模 ...

  2. 第十四篇:JavaScript

    本篇内容 简介 使用 DOM 一. 简介 JavaScript 是世界上最流行的编程语言. 这门语言可用于 HTML 和 web,更可广泛用于服务器.PC.笔记本电脑.平板电脑和智能手机等设备. Ja ...

  3. 洛谷 P2485 [SDOI2011]计算器 解题报告

    P2485 [SDOI2011]计算器 题目描述 你被要求设计一个计算器完成以下三项任务: 1.给定y.z.p,计算y^z mod p 的值: 2.给定y.z.p,计算满足xy ≡z(mod p)的最 ...

  4. [转]busybox登陆后没要求输入密码的解决办法

    转自:http://blog.chinaunix.net/uid-8058395-id-65785.html 1.制作好ramdisk之后 通过串口进入系统 却发现系统直接登录进去了 并没有要求用ro ...

  5. 每天一个小算法(Shell Sort1)

    希尔排序是1959 年由D.L.Shell 提出来的,相对直接排序有较大的改进.希尔排序又叫缩小增量排序 基本思想: 先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录 ...

  6. Codeforces:Good Bye 2018(题解)

    Good Bye 2018! 题目链接:https://codeforces.com/contest/1091 A. New Year and the Christmas Ornament 题意: 给 ...

  7. tornado获取application/json类型的入参

    tornado本身是不支持直接获取json入参的,在BaseHandler中定义方法get_json_argument,以供调用 class BaseHandler(tornado.web.Reque ...

  8. Linux中权限(r、w、x)对于目录与文件的意义

    Linux中权限(r.w.x)对于目录与文件的意义 一.权限对于目录的意义 1.首先要明白的是目录主要的内容是记录文件名列表和子目录列表,而不是实际存放数据的地方. 2.r权限:拥有此权限表示可以读取 ...

  9. [ CodeVS冲杯之路 ] P1295

    不充钱,你怎么AC? 题目:http://codevs.cn/problem/1295/ 数据很小,直接DFS,加上剪枝 剪枝其实就是判重,首先深度是行下标,这里自带不重复,枚举的列下标,用 f 记录 ...

  10. iphone CGBitmapContextCreate()函数解释

    http://blog.sina.com.cn/s/blog_3e50cef401019cd2.html CGContextRef CGBitmapContextCreate ( void *data ...