Java学习笔记之多线程二

  看到一篇讲线程的故事性文章,觉得很有意思,很佩服作者能这么生动地讲述出来,点击可跳转阅读此文章:《我是一个线程》

  

  继续我的笔记中总结 - -

理解线程安全问题:

  下面是书上看到的卖票例子:模拟3个窗口同时在售10张票。

  上篇博文笔记总结了多线程创建的两种方式,那我们就分别以这两种实现多线程的方式来解决这个场景。

  • 使用继承于Thread类的方式

  上Demo:

class SaleTicket extends Thread {
    int num = 10;  // 票数

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

    @Override
    public void run() {
        while (true) {
            if (num > 0) {
                System.out.println(Thread.currentThread().getName() + "窗口售出了第" + num + "号票" );
                try {
                    sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                num--;
            } else {
                System.out.println("售罄了...");
                break;
            }
        }
    }
}
public class Demo1 {
    public static void main(String[] args) {
        //创建三个线程对象
        SaleTicket th1 = new SaleTicket("A");
        SaleTicket th2 = new SaleTicket("B");
        SaleTicket th3 = new SaleTicket("C");
        //开启线程
        th1.start();
        th2.start();
        th3.start();
    }
}

  Dmeo1执行结果图为:

  

  从实验结果得到,本来10张的门票却卖出了30张,Why?

原来,这是因为:票数 num 声明为了一个非静态成员变量,而非静态成员变量是在每个对象中都会维护一份数据的。那么创建了三个线程对象就有三个num变量了~即我们这样要使得票数num为三个线程共享一份,因而需要将票数num变量设置为静态成员变量。即:

static int num = 10;//票数  非静态的成员变量,非静态的成员变量数据是在每个对象中都会维护一份数据的。

  改完后再运行之,结果如下:

    

  这时候发现确实输出的是10条记录(除去头三条可能出错的三条),说明static起作用,达到了共享的目的。

但细细一看,新的问题随之而来,A、B、C窗口居然会出现售出同一张票的情况!Why?

  这便搭上了今天的主题了,线程安全问题。

  分析如下:

  假设首先A线程抢夺了cpu的执行权,开始执行,当执行到

System.out.println(Thread.currentThread().getName() + "窗口售出了第" + num + "号票" );

   后(还未执行num--),num为10,此时B线程抢占了cpu执行权开始执行,同样执行到

System.out.println(Thread.currentThread().getName() + "窗口售出了第" + num + "号票" );

   后,num为50,C线程抢夺了cpu执行权,C同样执行到AB执行的相同的代码处,此时ABC三者num值都为10;因而才会出现以上现象。

   那么就得考虑,如何保证当一个线程在执行整块的代码时,不受其他线程的干扰?

于是乎,Java提供了一种同步机制来解决线程安全问题。

   同步机制分为同步代码块和同步方法

   先来看同步代码块的格式:

synchronized(锁对象){
    //需要被同步的代码
    ...
}

  当一个线程要使用火车票这个资源时,我们就交给它一把锁,等它把事情做完后再把锁给另一个需要用这个资源的线程.

  此时将上面分析的有可能引发线程安全问题的代码放在synchronized代码块中:

synchronized (SaleTicket.class) {
    if (num > 0) {
        System.out.println(Thread.currentThread().getName() + "窗口售出了第" + num + "号票" );
        try {
            sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        num--;
    } else {
            System.out.println("售罄了...");
        break;
    }
}

  再次运行之:

    

  由此得到了我们想要的正确结果。

  同步代码块要注意的事项:

    1. 锁对象可以是任意的一个对象。

    2. 一个线程在同步代码块中sleep了,并不会释放锁对象。

    3. 锁对象必须是多线程共享的一个资源,否则锁不住。

    4. 如果不存在着线程安全问题,千万不要使用同步代码块,因为会降低效率。

  同步代码块也可以使用同步方法的方式来实现:

  同步方法即将可能会发生线程安全问题的代码置于一个函数中,上面的实现方式为:

public void run() {
    while (true) {
        if (num > 0) {
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.sale();
        } else {
            break;
        }
    }
}// 同步方法
public synchronized void sale() {
    if(num > 0) {
        System.out.println(Thread.currentThread().getName() + "窗口售出了第" + (num--) + "号票" );
    }
}

  同步函数要注意的事项 :

    1. 如果是一个非静态的同步函数的锁 对象是this对象,如果是静态的同步函数的锁 对象是当前函数所属的类的字节码文件(class对象)。

    2. 同步函数的锁对象是固定的,不能随便指定的。

  那么,对于这两种实现同步机制的方式,推荐使用的是:同步代码块

  原因也很简单:

    1. 同步代码块的锁对象可以由我们随意指定,方便控制。而同步方法的锁对象是固定的。

    2. 同步代码块可以很方便控制需要被同步代码的范围,同步函数必须是整个函数 的所有代码都被同步了。

  • 使用实现Runnable接口的方式:

  Demo2:

class SaleTicket2 implements Runnable {
    int num = 10;

    @Override
    public void run() {
        while (true) {
            synchronized (SaleTicket2.class) {
                if (num > 0) {
                    System.out.println(Thread.currentThread().getName() + "窗口售出了第" + num + "号票");
                    num--;
                } else {
                    break;
                }
            }
        }
    }
}
public class Demo2 {
    public static void main(String[] args) {
        //创建了一个Runnable实现类的对象
        SaleTicket2 saleTicket = new SaleTicket2();
        //创建三个线程对象模拟三个窗口
        Thread th1 = new Thread(saleTicket, "A");
        Thread th2 = new Thread(saleTicket, "B");
        Thread th3 = new Thread(saleTicket, "C");
        //开启线程售票
        th1.start();
        th2.start();
        th3.start();
    }
}

  运行之,结果如下:

     

  同样也得到了正确的结果。

  总结:

  引发线程安全问题的根本原因是什么?

    1. 存在两个或两个以上的线程对象,并且线程之间共享同一个资源。

    2. 有多条语句操作了共享资源。

  解决方案:

    使用同步机制来解决,即通过同步代码块(推荐)或同步方法将可能引发线程安全问题的代码段“锁”住。

时间: 2024-10-25 08:31:58

Java学习笔记之多线程二的相关文章

java学习笔记(十二)多线程编程

并发编程:可以让多个任务同时运行的编程方式. 进程:一个正在运行的程序,有自己独立的一块内存空间,每一个进程的内部数据和状态都是完全独立的.可同时运行两个或更多的程序. 线程:每一个任务称为一个线程,在一个程序内运行多线程的程序称为多线程程序. 线程是进程内部单一的一个顺序控制流,是最小处理单位. 创建线程: 1.继承Thread类,重写run()方法,在run()方法中编写要执行的代码. 先创建Thread对象,打点调用start()启动线程. 2.实现Runable接口,只有run()方法.

Java学习笔记-8.多线程编程

一.引入线程 1.多线程和多进程的区别 (1)两者粒度不同,进程是由操作系统来管理,而线程则是在一个进程内 (2)每个进程是操作系统分配资源和处理器调度的基本单位,拥有独立的代码.内部数据和状态 而一个进程内的多线程只是处理器调度的基本单位,共享该进程的资源,线程间有可能相互影响 (3)线程本身的数据通常只有寄存器数据,以及一个程序执行时使用的堆栈,所以线程的切换比进程切换的负担小 2.Thread类:Java的线程是通过java.lang.Thread类来实现,一个Thread对象代表一个线程

黑马程序员——JAVA学习笔记六(多线程)

1,    什么是多线程?一个程序可以执行多个任务,每一个任务称为一个线程,运行多个线程的程序称为多线程程序. 进程:正在进行中的程序(直译). 线程:进程中一个负责程序执行的控制单元(执行路径).   多线程的好处:解决了多部分代码同时运行的问题.多线程的弊端:线程太多,会导致效率的降低. 其实,多个应用程序同时执行都是CPU在做着快速的切换完成的.这个切换是随机的.CPU的切换是需要花费时间的,从而导致了效率的降低 2 ,    创建线程方式:  创建线程方式一:继承Thread类 1.定义

Java学习笔记之多线程

/* 进程: 正在进行中的程序(直译). 线程: 就是进程中一个负责程序执行的控制单元(执行路径) 一个进程中可以有多个执行路径, 称之为多线程. 一个进程中至少要有一个线程. 开启多个线程是为了同时运行多部分代码. 每一个线程都有自己运行的内容. 这个内容可以称为线程要执行的任务. 多线程的好处: 解决了多部分同时运行的问题. 多线程的弊端: 线程太多回到效率的降低. 其实应用程序的执行都是cpu在做着快速的切换完成的. 这个切换是随机的. jvm启动时就启动了多个线程,至少有两个线程可以分析

Java学习笔记—第十二章 Java网络编程入门

第十二章  Java网络编程入门 Java提供的三大类网络功能: (1)URL和URLConnection:三大类中最高级的一种,通过URL网络资源表达方式,可以很容易确定网络上数据的位置.利用URL的表示和建立,Java程序可以直接读入网络上所放的数据,或把自己的数据传送到网络的另一端. (2)Socket:又称"套接字",用于描述IP地址和端口(在Internet中,网络中的每台主机都有一个唯一的IP地址,而每台主机又通过提供多个不同端口来提供多种服务).在客户/服务器网络中,当客

Java读书笔记(4)-多线程(二)

2016-1-2 线程通信 传统的线程通信 Object类提供了wait(),notify()和notifyAll三个方法 适用情况:synchronized修饰的同步方法或者synchronized方法 wait():导致当前线程等待,直到其他线程调用该同步监视器的notify()或notifyAll方法来唤醒该线程,调用wait方法后本线程会释放对同步监视器的锁定 notify():唤醒在此同步监视器上等待的单个线程.如果有多个线程在等待,则随机唤醒其中一个 notifyAll():唤醒在此

JAVA学习笔记(三十二)- 字符流 FileReader & FileWriter

标题 import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream;

JAVA学习笔记之多线程专题(一):线程同步安全处理

关于多线程操作,我相信大家都不陌生,如何开启一个线程之类我想就不用太详细的去描述,今天我们就来讲讲线程同步的安全的问题. 对于线程同步安全问题,一般是一个多线程对同一个资源同时操作的时候,会出现资源同时操作造成线程不安全的问题.那么这个时候我们需要去对公共资源进行同步保护.这个时候有三种情况 1.同步代码块,这个同步的锁是任意一个对象: 2.方法同步,这个同步的锁就是该方法所在的类: 3.静态方法同步,这个同步的锁是该方法所在类的字节码. 接下来,我们举一个例子来说明多线程对同一个资源进行操作的

黑马程序员——JAVA学习笔记十三(高新技术二)

8,    注解 Annotation(注解)是JDK5.0及以后版本引入的. 注解是以 @注解名 的形式标识 注解不会影响程序语义,只作为标识 注解是新的类型(与接口很相似),它与类.接口.枚举是在同一个层次,它们都称作为java的一个类型(TYPE). 它可以声明在包.类.字段.方法.局部变量.方法参数等的前面,用来对这些元素进行说明,注释. 它的作用非常的多,例如:进行编译检查.生成说明文档.代码分析等 注释类型 是一种特殊的接口 用 @interface 声明如 [email prote