Java从入门到精通十二(java线程)

  • 计算机操作系统的有关线程和进程的浅显说明
  • java执行方面的进程和线程的体现
  • java线程
    • Thread类信息摘要
    • 创建线程
      • 基于Thread类创建继承类
      • 为什么要重写run()方法?
      • start()方法如何调用run()方法?
      • 直接调用run()启动和通过start()启动线程的区别
      • 通过实现Runnable接口实现创建线程
      • 通过 实现Callable接口创建线程
    • 多线程卖票出现的问题
      • 1:为什么会卖出重复票?
      • 2:为什么可能会出现负数或者0张票?
      • 3:卖票案例数据安全问题的尝试解决
          • 同步代码块
          • 同步方法
          • 静态同步方法锁(类锁的一种形式)
          • 修饰类得到的类锁
          • lock加锁
    • 生产者消费者问题
      • 一部分的说明
      • 生产牛奶和消费牛奶

计算机操作系统的有关线程和进程的浅显说明

按照操作系统的理解,进程是操作系统分配资源的基本单位。
线程是调度资源的基本单位。

有很多形象的比喻,其实还是抽象化了。抽象化隐藏细节,实现似乎具体的可观。这样就是帮助理解。是在软件层次上的理解。

把一个进程比作一个车间,然后车间的工人就是就相当于线程,然后其实多线程的化,你就向大的级别进行,多线程就是相当于一个大厂,然后大厂里的多个车间就是进程。进程是车间,车间的资源提供给线程进行共享。然后执行每个零碎的任务。

一种非常标准的说法就是,进程就是程序的一次执行。打开一个程序,然后一个或者多个进程就开始了。然后这个程序具体功能的实现还是需要线程去具体实现的。

尽管这样说明,但是其实相信好多人还是和我有好多的疑问。因为根本的底层我们是不太理解的,或者是不理解。我的模电数电,组成原理没学好。汇编,操作系统的系统支撑了我的一些二对于底层的模糊认识。

在电脑上,我们在任务管理器可以直接查看进程。
所以说这些执行程序都可以认为是进程,基本的执行进程下面都有相应的线程。

还可以设置优先级

还可以设置哪些处理器运行指定的进程。

我的cpu不是八核的,这里是虚拟出来的,实际上只有四个内核。因为是intel,采用了超线程技术。

有的进程执行的时候只含有一个线程。举个例子。栗子很多,那么你到底想吃哪个?
开启一个终端,就是开启这样一个cmd.exe。cmd.exe是比较简单的程序,所以只需要这样一个线程。这也说明了它的功能就是比较单一的。等待用户输入命令,然后执行。

但是并不是一直一个,我在观察的时候会发现有时候会出现四个线程。我观察到,过一会儿会有规律的变成一个线程,即使我在终端执行命令,有时候它还是一个线程。

上面我们说了,线程就像车间忙碌的工人,大一点的程序一定会有多条线程的。

这样告诉我线程数就完了吗?我还想比较直观的看到线程的参数,以及优先级,我还想看到它的状态。

系统是一定会让你在终端查看进程的。我要先看进程。为什么我要先看进程,因为我可以通过进程的标识,查看它下面对应的进程。



提供一些参数

Pri:优先级
Thd:线程数
Hnd:句柄数
VM:虚拟内存
WS:工作集
Priv:专用虚拟内存
Priv Pk:专用虚拟内存峰值
Faults:页面错误
NonP:非页面缓冲池
Page:页面缓冲池
Cswtch:上下文切换

简单了解一下就可以,来举这个例子就是为了直观看到这些线程。感受一下。操纵系统中常说的阻塞或者排队状态,上面也给出了标识。

页面错误在组成原理提到过

若进程欲访问的页面目前并未驻留在内存中,将会发生页面错误

页面错误很正常,这里代表缺页。当发生缺页后,就会触发页面错误
当你的进程某些数据的页面长时间不被访问后,Windows将把它们从内存置换到硬盘上,常用的那些页面则常驻内存。但是需要用到那些数据的时候,发现内存中没有那些页面,就发生缺页中断,然后从硬盘上把那些页面调入。

我清楚的记得组成原理有提到过。果然听听还是有很大用的。

java执行方面的进程和线程的体现

我们会想到java基本的运行机制

java源代码首先需要通过java编译器编译为字节码文件(.class文件),字节码文件是一种二进制的文件,里面的数据紧密相连。文件的内容比较紧凑。字节码文件时通过javac .exe生成,然后再java.exe的作用下进行启动虚拟机(jvm),执行字节码。虚拟机会将编译好的字节码文件加载到内存(也就是类加载。然后虚拟机会对这个类加载的文件进行解释执行。)
直接由虚拟机进行解释执行,而并非操作系统。这样特点也决定了它的跨平台(java程序通过jvm实现)。

jvm也是一个软件层次的程序,在功能的实现上,具有自己完整的堆栈存储区,数据存储区这些,计数器等等。因此有时候也可以被认为是一个小型的计算机。但是这些都是虚拟出来的,实在软件的层次上虚拟出来的功能架构。

jvm是用来具体执行java程序的。jvm执行的时候本身也是一个进程的的进程。并且它是一个多线程对我进程。因为jvm需要做一些事情来支持java的运行机制。

所以jvm本身就是一个多线程的应用。从我们执行程序的main方法入口开始。jvm程序需要执行的时候,操作系统将jvm从磁盘存储器将其调入到内存中,然后创建一个jvm的进程。jvm启动主线程,主线程调用类的main方法,所以主线程也就是从main方法这里开始执行了。既然是一个多线程的应用,那么除了主线程以外还有其它的线程。

比如比较经典的垃圾回收机制的GC thread,此类线程主要进行对JVM里面的垃圾进行收集。再比如字节码编译的线程Compiler threads,再比如给jvm收集信号的线程Singal dispatcher thread。

好奇线程是如何调用的,实现的底层是什么?这就给自己又添麻烦了。

自己去查看start()这个启动的源码

这个函数的里面调用的另一个函数start0才应该是真正启动线程的。底层还是c++写的。native修饰说明是调用了原生系统函数。我去看了看,觉得还是挺复杂的。用notepad打开比较快一点

一个jvm.cpp就将近四千行代码,所以我大致看了几个函数就走了。再见!

参考了网上的一些说明,大致都是在说明方法调用逻辑。

线程启动过程。这是在c++源码上来说的。

说明:该图来自
小傅哥
CodeGuide | 程序员编码指南 Go! - 沉淀、分享、成长,让自己和他人都能有所收获!

此文有对源码,以及具体执行过程的一些探索。

java自己携带了一个可视化的工具,可以动态的观察一下cpu占用,堆,线程的情况。当然这些不是目前主要去看的,主要用在后面的性能调优上面。

VisualVM,java自带的一个监控工具

主要可以分析数据,跟踪内存泄漏,监控垃圾回收,执行内存等等的操作。

直接在控制台运行jvisualvm大体就是这个界面。当然可以去进行远程连接,查看资源情况等等。


当你在运行一个程序的时候,比较大的一个程序,可以去看看jvm的堆占用情况。

java线程

Thread类信息摘要

public class Threadextends Objectimplements Runnable
线程 是程序中的执行线程。Java 虚拟机允许应用程序并发地运行多个执行线程。

创建新执行线程有两种方法。一种方法是将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。接下来可以分配并启动该子类的实例。

创建线程的另一种方法是声明实现 Runnable 接口的类。该类然后实现 run 方法。然后可以分配该类的实例,在创建 Thread 时作为一个参数来传递并启动。

每个线程都有一个标识名,多个线程可以同名。如果线程创建时没有指定标识名,就会为其生成一个新名称。

一:嵌套类说明

public static enum Thread.Stateextends Enum<Thread.State>

该类的枚举常量说明了线程的状态

BLOCKED
受阻塞并且正在等待监视器锁的某一线程的线程状态。
NEW
至今尚未启动的线程的状态。
RUNNABLE
可运行线程的线程状态。
TERMINATED
已终止线程的线程状态。
TIMED_WAITING
具有指定等待时间的某一等待线程的线程状态。
WAITING
某一等待线程的线程状态。

另外一个嵌套类

public static interface Thread.UncaughtExceptionHandler

这是一个接口

当 Thread 因未捕获的异常而突然终止时,调用处理程序的接口。

当某一线程因未捕获的异常而即将终止时,Java 虚拟机将使用 Thread.getUncaughtExceptionHandler() 查询该线程以获得其 UncaughtExceptionHandler 的线程,并调用处理程序的 uncaughtException 方法,将线程和异常作为参数传递。如果某一线程没有明确设置其 UncaughtExceptionHandler,则将它的 ThreadGroup 对象作为其 UncaughtExceptionHandler。如果 ThreadGroup 对象对处理异常没有什么特殊要求,那么它可以将调用转发给默认的未捕获异常处理程序。

二:主要的一些字段

static int MAX_PRIORITY
线程可以具有的最高优先级。
static int MIN_PRIORITY
线程可以具有的最低优先级。
static int NORM_PRIORITY
分配给线程的默认优先级。

优先级可以自己进行设定,有一个范围,也可以去获取运行线程的优先级。

三:构造方法

Thread()
分配新的 Thread 对象。
Thread(Runnable target)
分配新的 Thread 对象。
Thread(Runnable target, String name)
分配新的 Thread 对象。
Thread(String name)
分配新的 Thread 对象。
Thread(ThreadGroup group, Runnable target)
分配新的 Thread 对象。
Thread(ThreadGroup group, Runnable target, String name)
分配新的 Thread 对象,以便将 target 作为其运行对象,将指定的 name 作为其名称,并作为 group 所引用的线程组的一员。
Thread(ThreadGroup group, Runnable target, String name, long stackSize)
分配新的 Thread 对象,以便将 target 作为其运行对象,将指定的 name 作为其名称,作为 group 所引用的线程组的一员,并具有指定的堆栈大小。
Thread(ThreadGroup group, String name)
分配新的 Thread 对象。

四:方法

static int activeCount()
返回当前线程的线程组中活动线程的数目。
void checkAccess()
判定当前运行的线程是否有权修改该线程。
int countStackFrames()
已过时。 该调用的定义依赖于 suspend(),但它遭到了反对。此外,该调用的结果从来都不是意义明确的。
static Thread currentThread()
返回对当前正在执行的线程对象的引用。
void destroy()
已过时。 该方法最初用于破坏该线程,但不作任何清除。它所保持的任何监视器都会保持锁定状态。不过,该方法决不会被实现。即使要实现,它也极有可能以 suspend() 方式被死锁。如果目标线程被破坏时保持一个保护关键系统资源的锁,则任何线程在任何时候都无法再次访问该资源。如果另一个线程曾试图锁定该资源,则会出现死锁。这类死锁通常会证明它们自己是“冻结”的进程。有关更多信息,请参阅为何不赞成使用 Thread.stop、Thread.suspend 和 Thread.resume?。
static void dumpStack()
将当前线程的堆栈跟踪打印至标准错误流。
static int enumerate(Thread[] tarray)
将当前线程的线程组及其子组中的每一个活动线程复制到指定的数组中。
static Map<Thread,StackTraceElement[]> getAllStackTraces()
返回所有活动线程的堆栈跟踪的一个映射。
ClassLoader getContextClassLoader()
返回该线程的上下文 ClassLoader。
static Thread.UncaughtExceptionHandler getDefaultUncaughtExceptionHandler()
返回线程由于未捕获到异常而突然终止时调用的默认处理程序。
long getId()
返回该线程的标识符。
String getName()
返回该线程的名称。
int getPriority()
返回线程的优先级。
StackTraceElement[] getStackTrace()
返回一个表示该线程堆栈转储的堆栈跟踪元素数组。
Thread.State getState()
返回该线程的状态。
ThreadGroup getThreadGroup()
返回该线程所属的线程组。
Thread.UncaughtExceptionHandler getUncaughtExceptionHandler()
返回该线程由于未捕获到异常而突然终止时调用的处理程序。
static boolean holdsLock(Object obj)
当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。
void interrupt()
中断线程。
static boolean interrupted()
测试当前线程是否已经中断。
boolean isAlive()
测试线程是否处于活动状态。
boolean isDaemon()
测试该线程是否为守护线程。
boolean isInterrupted()
测试线程是否已经中断。
void join()
等待该线程终止。
void join(long millis)
等待该线程终止的时间最长为 millis 毫秒。
void join(long millis, int nanos)
等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。
void resume()
已过时。 该方法只与 suspend() 一起使用,但 suspend() 已经遭到反对,因为它具有死锁倾向。有关更多信息,请参阅为何不赞成使用 Thread.stop、Thread.suspend 和 Thread.resume?。
void run()
如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。
void setContextClassLoader(ClassLoader cl)
设置该线程的上下文 ClassLoader。
void setDaemon(boolean on)
将该线程标记为守护线程或用户线程。
static void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
设置当线程由于未捕获到异常而突然终止,并且没有为该线程定义其他处理程序时所调用的默认处理程序。
void setName(String name)
改变线程名称,使之与参数 name 相同。
void setPriority(int newPriority)
更改线程的优先级。
void setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
设置该线程由于未捕获到异常而突然终止时调用的处理程序。
static void sleep(long millis)
在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
static void sleep(long millis, int nanos)
在指定的毫秒数加指定的纳秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
void start()
使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
void stop()
已过时。 该方法具有固有的不安全性。用 Thread.stop 来终止线程将释放它已经锁定的所有监视器(作为沿堆栈向上传播的未检查 ThreadDeath 异常的一个自然后果)。如果以前受这些监视器保护的任何对象都处于一种不一致的状态,则损坏的对象将对其他线程可见,这有可能导致任意的行为。stop 的许多使用都应由只修改某些变量以指示目标线程应该停止运行的代码来取代。目标线程应定期检查该变量,并且如果该变量指示它要停止运行,则从其运行方法依次返回。如果目标线程等待很长时间(例如基于一个条件变量),则应使用 interrupt 方法来中断该等待。有关更多信息,请参阅为何不赞成使用 Thread.stop、Thread.suspend 和 Thread.resume?。
void stop(Throwable obj)
已过时。 该方法具有固有的不安全性。有关详细信息,请参阅 stop()。该方法的附加危险是它可用于生成目标线程未准备处理的异常(包括若没有该方法该线程不太可能抛出的已检查的异常)。有关更多信息,请参阅为何不赞成使用 Thread.stop、Thread.suspend 和 Thread.resume?。
void suspend()
已过时。 该方法已经遭到反对,因为它具有固有的死锁倾向。如果目标线程挂起时在保护关键系统资源的监视器上保持有锁,则在目标线程重新开始以前任何线程都不能访问该资源。如果重新开始目标线程的线程想在调用 resume 之前锁定该监视器,则会发生死锁。这类死锁通常会证明自己是“冻结”的进程。有关更多信息,请参阅为何不赞成使用 Thread.stop、Thread.suspend 和 Thread.resume?。
String toString()
返回该线程的字符串表示形式,包括线程名称、优先级和线程组。
static void yield()
暂停当前正在执行的线程对象,并执行其他线程。

以上引用摘自javaapi

具体的使用说明的话,将说明总结一些比较常用的方法进行举例代码。

创建线程

基于Thread类创建继承类

再次回到Thread类

public class Threade xtends Object implements Runnable

这个类实现的是Runnable接口

public class Threadextends Objectimplements Runnable

Runnable 接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为 run 的无参数方法。

Runnable 为非 Thread 子类的类提供了一种激活方式。通过实例化某个 Thread 实例并将自身作为运行目标,就可以运行实现 Runnable 的类而无需创建 Thread 的子类。大多数情况下,如果只想重写 run() 方法,而不重写其他 Thread 方法,那么应使用 Runnable 接口。这很重要,因为除非程序员打算修改或增强类的基本行为,否则不应为该类创建子类。

我们进行创建线程的时候,第一种方式就是基于Thread进行一个继承。

使用该方法需要继承Thread类,然后进行重写run方法(多数情况下需要重写run方法。)

package process;

public class ThreadPratice01  extends Thread {
    @Override
    public void run() {
       // super.run();
       for(int i = 0;i<100;i++)
       {

           System.out.println(Thread.currentThread().getName()+":"+i);
           try {
               Thread.sleep(1000);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }

       }
    }

    public static void main(String args[])
    {

        ThreadPratice01 th1= new ThreadPratice01();
        ThreadPratice01 th2 = new ThreadPratice01();
        th1.start();
        th2.start();

    }
}

线程会进行竞争占用分配资源。在一定程度上取决于cpu时间片分配。在没有人为进行设置优先级的情况下,基本对资源的占用几率是一样的,即使设置了较高的优先级,也只能说明有更大的几率抢占到资源。

有一个问题,为什么要去重写run()方法?

为什么要重写run()方法?

首先来看看这个run()方法究竟做了什么事情?(没有重写前)
首先,这是Thread的对接口类Runnable的抽象run()方法的一个实现。


可以了解下target是什么?

可以看到target其实是一个Runnable类型的对象。我们一般在自己创建线程类的时候就会使用到Thread或者Tunnable类型的对象进行传入,然后实现对方法的一个调用。

但是其实你发现这个Thread实现的的run()方法其实也没有做什么事情。

当然如果你不重写run()方法的话并不是不可以,至少在语法上这是没有问题的,只是如果这样做的话,毫无意义。

如下这样启动线程的话是没有任何输出内容的。但是你要知道这里是默认调用了没有进行重写的run()方法的。


package process;

public class ThreadPratice01  extends Thread {

       // super.run();



    public static void main(String args[])
    {

        ThreadPratice01 th1= new ThreadPratice01();
        ThreadPratice01 th2 = new ThreadPratice01();
        th1.start();
        th2.start();

    }
}

所以我们一般是需要对run()方法进行重写的,这样自己定义一个启动执行的多线程程序。

start()方法如何调用run()方法?

找来找去,也查看了一些说明,原来还是涉及到c++
首先还是和前面的start0()这个方法有关系。


native在这里进行修饰说明了这里调用了一个非java语言实现的接口。java需要提供一个java和本地的c语言代码进行相互操作的接口,一般简称为JNI(java Native Interface),这样调用底层的c++的接口,这里面的一些加载的过程全部交给jvm进行操作。


registerNatives()是一个注册的方法,位于static修饰的静态代码块中。这样的作用就是在Thread类被jvm直接运行。在类初始化的时候会执行一次,并且执行的优先级高于非静态的代码块。也就是说会在类加载的时候直接执行。然后就执行相应的功能,就是实现一个注册。

具体的话可以在openjdk进行查看源代码。(代码非常多)

但是可以大致得出就是start通过jvm来实现对run()方法的调用。当然这似乎就是废话。主要是在jvm的一些底层实现上。

然后我去进行查找。

抱歉,前面似乎说的有一些问题。一部分Thread是c,一部分是c++。
这个在Thread.c文件中。我开始还去Thread.cpp去找。结果在c++代码中没有找到。

这个Thread.c的代码只有七十多行代码,其中有一些还是注释。

/*
 * Copyright (c) 1994, 2012, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle if you need additional information or have any
 * questions.
 */

/*-
 *      Stuff for dealing with threads.
 *      originally in threadruntime.c, Sun Sep 22 12:09:39 1991
 */

#include "jni.h"
#include "jvm.h"

#include "java_lang_Thread.h"

#define THD "Ljava/lang/Thread;"
#define OBJ "Ljava/lang/Object;"
#define STE "Ljava/lang/StackTraceElement;"
#define STR "Ljava/lang/String;"

#define ARRAY_LENGTH(a) (sizeof(a)/sizeof(a[0]))

static JNINativeMethod methods[] = {
    {"start0",           "()V",        (void *)&JVM_StartThread},
    {"stop0",            "(" OBJ ")V", (void *)&JVM_StopThread},
    {"isAlive",          "()Z",        (void *)&JVM_IsThreadAlive},
    {"suspend0",         "()V",        (void *)&JVM_SuspendThread},
    {"resume0",          "()V",        (void *)&JVM_ResumeThread},
    {"setPriority0",     "(I)V",       (void *)&JVM_SetThreadPriority},
    {"yield",            "()V",        (void *)&JVM_Yield},
    {"sleep",            "(J)V",       (void *)&JVM_Sleep},
    {"currentThread",    "()" THD,     (void *)&JVM_CurrentThread},
    {"countStackFrames", "()I",        (void *)&JVM_CountStackFrames},
    {"interrupt0",       "()V",        (void *)&JVM_Interrupt},
    {"isInterrupted",    "(Z)Z",       (void *)&JVM_IsInterrupted},
    {"holdsLock",        "(" OBJ ")Z", (void *)&JVM_HoldsLock},
    {"getThreads",        "()[" THD,   (void *)&JVM_GetAllThreads},
    {"dumpThreads",      "([" THD ")[[" STE, (void *)&JVM_DumpThreads},
    {"setNativeName",    "(" STR ")V", (void *)&JVM_SetNativeThreadName},
};

#undef THD
#undef OBJ
#undef STE
#undef STR

JNIEXPORT void JNICALL
Java_java_lang_Thread_registerNatives(JNIEnv *env, jclass cls)
{
    (*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));
}

从上面代码可以了解到调用的start方法,会对应调用JVM_StartThread,我们还可以找到对应熟悉的方法,相必stop0也是同样的道理。再具体的话,真的就不想再去看了。说了这么多,只是为了说明这个调用的方式。和底层的实现有关,还有一个非java的接口。提供对应的注册调用。

在此基础上我们可以尝试一些线程相关的方法

package process;
//实现多线程
public class MyThread extends Thread {
    //也可以自己设置无参和带参构造方法
//    这样在进行创建对象的时候可以直接进行传值
    public MyThread() {

    }

    public MyThread(String name) {
        super(name);//调用父类的这个构造方法,这样就可以在new实例化的时候传入字符串。
    }

    @Override
    //run方法封装线程执行的代码
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + ":" + i);
            try {
                Thread.sleep(1000);//线程休眠
            } catch (InterruptedException e) {
                e.printStackTrace();
            }


        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread my1 = new MyThread();//这样就可以选择时候在创建对象的时候是否进行传入值
        MyThread my2 = new MyThread();
        //获取线程的优先级
        System.out.println(my1.getPriority());//都是5
        System.out.println(my2.getPriority());
        my1.setName("高铁");//给对应的线程设置名字
        my2.setName("飞机");

        //设置一个主线程
        currentThread().setName("刘备");
        System.out.println(Thread.MAX_PRIORITY);//线程最大优先级
        System.out.println(Thread.MIN_PRIORITY);//线程最小优先级
        System.out.println(Thread.NORM_PRIORITY);//线程默认优先级
        //设置优先级
        my1.setPriority(6);
        my2.setPriority(8);
        my1.join();//让调用他的线程先执行完,其它线程再执行。
        //线程优先级高只是代表获取cpu时间片的几率高
        //设置优先级
        my1.setDaemon(true);//设置守护线程
        //设置为守护线程,则会在主线程执行完后,守护线程也会马上执行完毕
        my1.start();
        //自己设置线程名称

        my2.start();
        System.out.println("获取main方法正在执行的线程名称:" + Thread.currentThread().getName());
        for (int i =0;i<10;i++)
        {
            System.out.println(Thread.currentThread().getName()+","+i);
        }

    }
}

但是在启动线程方面,海域一个疑惑,就是我们可以也可以进行调用run()方法,直接调用,不通过start(),这样在语法上是没有错误的,但是好奇这样做的话,会有什么样的区别。

直接调用run()启动和通过start()启动线程的区别

package process;

public class ThreadPratice01  extends Thread {

    @Override
    public void run() {
       for(int i =0;i<10;i++)
       {
           System.out.println(Thread.currentThread().getName()+":"+i);
           try {
               Thread.sleep(1000);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }
    }

    public static void main(String args[])
    {

        ThreadPratice01 th1= new ThreadPratice01();
        ThreadPratice01 th2 = new ThreadPratice01();
        //th1.start();不可以被重复调用哦
        //th1.start();
        th2.start();
        th1.run();



    }
}

这样执行后,其实你会发现程序th1.run()之后,可以发现程序的线程名是main,或者你单独运行它一下,这样的启动线程仍然是在main进行。所以根本就没有创建新的线程。

但是通过start调用的话,你会发现会得到一个新的创建的线程。

public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

关键要素还是start0()这里调用的非java实现的方法,前面对这个start0()有一些说明,就不再赘述。

通过实现Runnable接口实现创建线程

这样去创建线程的话,很明显的特点就是在实现Runnable接口的时候还可以去基础其它的类,显得更加灵活,更加符合java语言多态的特点。

package process;

public class MyThread01 implements Runnable {
    //通过实现Runnable接口来实现多线程

    public static void main(String[] args) {
        MyThread01 my = new MyThread01();
//        Thread t1 = new Thread(my);
//        Thread t2 = new Thread(my);
        //给线程起名字
        Thread t1 = new Thread(my, "高铁");
        Thread t2 = new Thread(my, "飞机");

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

    }

    @Override
    public void run() {
        for(int i =0;i<100;i++)
        {
            System.out.println(Thread.currentThread().getName()+":"+i);
            
        }
    }
    
}

使用到Thread类的构造方法

Thread(Runnable target, String name)
分配新的 Thread 对象。

通过 实现Callable接口创建线程

public interface Callable
返回结果并且可能抛出异常的任务。实现者定义了一个不带任何参数的叫做 call 的方法。

Callable 接口类似于 Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。但是 Runnable 不会返回结果,并且无法抛出经过检查的异常。

那就简单实现一下
这样实现的话还需要去靠一些接口类去实现。

package process;

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class MyThread02 implements Callable {
    public static void main(String[] args) {
        MyThread02 mt = new MyThread02();
        FutureTask ft = new FutureTask<>(mt);
        FutureTask ft1 = new FutureTask<>(mt);
        new Thread(ft).start();
        new Thread(ft1).start();

    }

    @Override
    public Object call() throws Exception {
        for(int i =1000;i>0;i--)
        {
            System.out.println(Thread.currentThread().getName()+";"+i);
        }
        return null;
    }
}


说明

public class FutureTaskextends Objectimplements RunnableFuture

所有已实现的接口:
Runnable, Future, RunnableFuture

可使用 FutureTask 包装 Callable 或 Runnable 对象。因为 FutureTask 实现了 Runnable,所以可将 FutureTask 提交给 Executor 执行。

多线程卖票出现的问题

当然这是一个很经典并且常用的例子了

package process;

public class SellTicketDemo implements Runnable {
    private int tickets = 100;

    public static void main(String[] args) {
        //创建对象
        SellTicketDemo st = new SellTicketDemo();
        Thread t1 = new Thread(st, "窗口一");
        Thread t2 = new Thread(st, "窗口二");
        Thread t3 = new Thread(st, "窗口三");
        //t1.setPriority(10);
        //启动线程
        t1.start();
        t2.start();
        t3.start();


    }

    //出现的问题:
//    相同的票出现了多次

    @Override
    public void run() {
        while (true) {
            if (tickets > 0) {
                //通过sleep()方法来模拟出票时间

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

                System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                tickets--;
            }

        }

    }
}


这个休眠时间可以设置不一样的时间,会出现不同的结果。(比较明显)

在我自己的电脑上最终尝试结果是会有卖出0张票,和一张票,在有的操作系统上,还会出现卖出-1张票。

这些不同的结果都与多线程抢占资源有关。

在看黑马教程的时候,老师给出了非常详细的说明

1:为什么会卖出重复票?

线程在执行代码语句后,都有可能被其它的线程抢占到资源。

2:为什么可能会出现负数或者0张票?


执行权的抢占决定了运行的结果,但是基本没有见过-2张票的情况。主要需要知道的是线程执行具有随机性。

3:卖票案例数据安全问题的尝试解决

同步代码块

数据安全问题主要是说明了对数据的处理不符合现实的预期情况。

多线程在没有进行自己处理的情况下,对数据的共享会出现问题,单线程是不会造成这种问题的。

解决办法就是让任一时刻只有一个线程执行。
同步代码块

按照这样的解决思路,我们可以这样去做

package process;

public class SellTicketDemo implements Runnable {
    private int tickets = 100;
    private Object obj = new Object();
    public static void main(String[] args) {
        //创建对象
        SellTicketDemo st = new SellTicketDemo();
        Thread t1 = new Thread(st, "窗口一");
        Thread t2 = new Thread(st, "窗口二");
        Thread t3 = new Thread(st, "窗口三");
        //t1.setPriority(10);
        //启动线程
        t1.start();
        t2.start();
        t3.start();

    }


    @Override
    public void run() {
        while (true) {
            synchronized (obj){
                if (tickets > 0) {
                    //通过sleep()方法来模拟出票时间

                    {

                    }

                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                    tickets--;
                    if(tickets==0)
                    {
                        System.out.println("票已经卖完!");
                        System.exit(0);
                    }
                }
            }


        }

    }
}


还要注意一点的就是如果我们把程序写成这样子行不行,我们将这里的循环体写成这样行不行?
注意这样也是不行的,这里就不是锁的问题了,主要就是程序设计的问题了,假如我们的一个窗口抢到线程的执行权,此时跑数为1,然后刚好进去循环体,但是呢,基于这个while循环限制条件,并没有将其他的线程限制在外面,抢到线程的窗口执行完最后一张票的时候,然后另外的一个线程其实已经在锁外等待,但是并不是在while循环体外面,第一个窗口将票数减少到0的时候,第二个线程会减少到-1。

 while (tickets > 0)
        {
            synchronized (object)
            {
                System.out.println(Thread.currentThread().getName()+"正在卖出第"+tickets+"张票");
                tickets--;
            }
        }

注意new一个锁对象的时候,不要进行重复new,不然就是重复加锁,这样做还是徒劳的。

采用同步的方式加锁,解决了多线程的数据安全的问题,但是在线程很多的时候,每个线程都会去判断同步的锁,这样会降低程序运行的效率。

当然你可以将代码块方法封装到一个定义地方法中,比如下面这样,但是这样不叫同步方法。

package process;

public class SellTicketDemo implements Runnable {
    private int tickets = 100;
    private Object obj = new Object();
    public static void main(String[] args) {
        //创建对象
        SellTicketDemo st = new SellTicketDemo();
        Thread t1 = new Thread(st, "窗口一");
        Thread t2 = new Thread(st, "窗口二");
        Thread t3 = new Thread(st, "窗口三");
        //t1.setPriority(10);
        //启动线程
        t1.start();
        t2.start();
        t3.start();
    }
    @Override
    public void run() {
       sellTicket();

    }
    private void sellTicket() {
        while (true) {
            synchronized (obj){
                if (tickets > 0) {
                    //通过sleep()方法来模拟出票时间
                    {
                    }
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                    tickets--;
                    if(tickets==0)
                    {
                        System.out.println("票已经卖完!");
                        System.exit(0);
                    }
                }
            }


        }
    }
}

也可以使用this关键字

package process;

public class SellTicketDemo implements Runnable {
    private int tickets = 1000;
    private Object obj = new Object();
    public static void main(String[] args) {
        //创建对象
        SellTicketDemo st = new SellTicketDemo();
        Thread t1 = new Thread(st, "窗口一");
        Thread t2 = new Thread(st, "窗口二");
        Thread t3 = new Thread(st, "窗口三");
        //t1.setPriority(10);
        //启动线程
        t1.start();
        t2.start();
        t3.start();
    }
    @Override
    public void run() {
        sellTicket();

    }
    private void sellTicket() {
        while (true) {
            synchronized (this){
                if (tickets > 0) {
                    //通过sleep()方法来模拟出票时间
                    {
                    }
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                    tickets--;
                    if(tickets==0)
                    {
                        System.out.println("票已经卖完!");
                        System.exit(0);
                    }
                }
            }


        }
    }
}

this指代了当前对象,也就是该类的实例。
这样是可以的,但是该段代码需要注意的是,如果while循环也位于锁内,其实效果还是相当于将整个方法锁定。结果与此不同。

同步方法


想办法把锁加到方法上

package process;

public class SellTicketDemo implements Runnable {
    private int tickets = 100;
    private Object obj = new Object();
    public static void main(String[] args) {
        //创建对象
        SellTicketDemo st = new SellTicketDemo();
        Thread t1 = new Thread(st, "窗口一");
        Thread t2 = new Thread(st, "窗口二");
        Thread t3 = new Thread(st, "窗口三");
        //t1.setPriority(10);
        //启动线程
        t1.start();
        t2.start();
        t3.start();
    }
    @Override
    public void run() {
       sellTicket();

    }
    private synchronized void sellTicket() {
        while (true) {

                if (tickets > 0) {
                    //通过sleep()方法来模拟出票时间
                    {
                    }
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                    tickets--;
                    if(tickets==0)
                    {
                        System.out.println("票已经卖完!");
                        System.exit(0);
                    }
                }



        }
    }
}


但是这样其实并没有达到想要的效果,只有一个线程可以执行完,一直到程序结束。这样还是将整个方法锁定了,所以线程不能实现交替执行。最终只有一个线程执行完后退出。

静态同步方法锁(类锁的一种形式)

主要体现在synchronized对静态方法地修饰上面。然后他锁定的话主要是对类进行锁定。

package process;

public class SellTicketDemo implements Runnable {
    static int  tickets = 1000;
    private Object obj = new Object();
    public static void main(String[] args) {
        //创建对象
        SellTicketDemo st = new SellTicketDemo();
        Thread t1 = new Thread(st, "窗口一");
        Thread t2 = new Thread(st, "窗口二");
        Thread t3 = new Thread(st, "窗口三");
        //t1.setPriority(10);
        //启动线程
        t1.start();
        t2.start();
        t3.start();
    }
    @Override
    public void run() {
        sellTicket();

    }
    private static synchronized void sellTicket() {
        while (true) {

                if (tickets > 0) {
                    //通过sleep()方法来模拟出票时间
                    {
                    }
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                    tickets--;
                    if(tickets==0)
                    {
                        System.out.println("票已经卖完!");
                        System.exit(0);
                    }
                }
            }


        }
    }

但是这样的话,是锁住了整个类,类中的资源需要等到开始抢占到资源的线程释放掉类锁后才允许执行。其它的线程也位于这个类中,当然是无法执行的。

修饰类得到的类锁
package process;

public class SellTicketDemo implements Runnable {
    private int   tickets = 1000;
    private Object obj = new Object();
    public static void main(String[] args) {
        //创建对象
        SellTicketDemo st = new SellTicketDemo();

        Thread t1 = new Thread(st, "窗口一");
        Thread t2 = new Thread(st, "窗口二");
        Thread t3 = new Thread(st, "窗口三");
        //t1.setPriority(10);
        //启动线程
        t1.start();
        t2.start();
        t3.start();
    }
    @Override
    public void run() {
        sellTicket();


    }

    private void sellTicket() {
        while (true) {
                synchronized (SellTicketDemo.class)
                {
                    if (tickets > 0) {
                        //通过sleep()方法来模拟出票时间
                        {
                        }
                        try {
                            Thread.sleep(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                        tickets--;
                        if(tickets==0)
                        {
                            System.out.println("票已经卖完!");
                            System.exit(0);
                        }
                    }
                }

            }


        }
    }

这样启动的话,事实证明这样的类锁也可以解决数据安全问题。(卖票案例)

这些都是锁的使用的一些很浅显的例子。

lock加锁

Lock接口的说明,相比较synchronized有什么特点呢?

Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 Condition 对象。

synchronized 方法或语句的使用提供了对与每个对象相关的隐式监视器锁的访问,但却强制所有锁获取和释放均要出现在一个块结构中:当获取了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的词法范围内释放所有锁。

lock接口的实现允许锁在不同的作用范围内获取和释放,并且允许以任何顺序获取和释放多个锁。这样就显得非常灵活。

使用格式

Lock l = ...; 
     l.lock();
     try {
         // access the resource protected by this lock
     } finally {
         l.unlock();
     }
 

提供了一些方法

1:void lock()
获取锁。
2: void lockInterruptibly()
如果当前线程未被中断,则获取锁。
3: Condition newCondition()
返回绑定到此 Lock 实例的新 Condition 实例。
4:boolean tryLock()
仅在调用时锁为空闲状态才获取该锁。
5: boolean tryLock(long time, TimeUnit unit)
如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁。
6:void unlock()
释放锁。

应该是java5之后推出的lock锁。
不过在本例中,我们只需要用到两个方法

针对上面的问题。再次尝试一种锁,lock锁。看看怎么用?

package process;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SellTicketDemo implements Runnable {
    private int tickets = 100;
    private Lock lock = new ReentrantLock();
    private Object obj = new Object();

    public static void main(String[] args) {
        //创建对象
        SellTicketDemo st = new SellTicketDemo();

        Thread t1 = new Thread(st, "窗口一");
        Thread t2 = new Thread(st, "窗口二");
        Thread t3 = new Thread(st, "窗口三");

        //t1.setPriority(10);
        //启动线程
        t1.start();
        t2.start();
        t3.start();
    }

    @Override
    public void run() {
        sellTicket();

    }

    private void sellTicket() {


        while (true) {

            try {
                lock.lock();
                if (tickets > 0) {
                    //通过sleep()方法来模拟出票时间

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                    tickets--;
                    if (tickets == 0) {
                        System.out.println("票已经卖完!");
                        System.exit(0);
                    }
                }
            } finally {
                lock.unlock();
            }



        }
    }

}



生产者消费者问题

一部分的说明

先来说说生产者消费者问题(计算机层次上,非生物科学)

生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了两个共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。

要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用进程间通信的方法解决该问题,常用的方法有信号灯法等。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。该问题也能被推广到多个生产者和消费者的情形。

以上摘自百度百科

这当然说的还是比较专业的,但是其实简单说就是生产者生产出资源,然后消费者使用。我把生产的资源放入存储柜,然后消费者去取。存在的问题可能就是供不应求,比如生产的不够消费者用,或者还有就是堵塞,我生产的过多,存储柜都快放不下了,消费者难以及时消费掉。

比较和谐的做法就是,如果我生产在存储柜里的资源足够的话,就让消费者去消费,如果资源不够的话,就让消费者等待,等我生产出再去取。

简单的说,就是这些,当然还有信号,对于死锁问题的说明等等,就不详细说明了。

生产牛奶和消费牛奶

还是在b看看视频学到的,然后自己浅显的加了点东西。
作为笔记,非常喜欢这个案例,虽然简单,但是这个例子和操作系统的pv等的操作很相似,可以说基本一样。对于自己对系统资源的这个管理认识也有一定的帮助,记录下来,以后可以自己再看看。

编写一个模拟程序

一个生产者类,一个消费者类,一个箱子类,一个测试类。

package process;

import java.util.Random;

public class Producer implements Runnable{
    private  Box b;
    public Producer(Box b) {
        this.b = b;
    }

    @Override
    public void run() {
        while(true)
        {
            Random random = new Random();
            int i = random.nextInt(10)+1;
            try {
                b.put(i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

//        for(int i= 1;i<=5;i++)
//        {
//            try {
//                b.put(i);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//        }

    }
}

package process;

public class Customer implements Runnable {
    private Box b;
    public Customer(Box b) {
        this.b = b;
    }

    @Override
    public void run() {
        while(true)
        {
            try {
                b.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}

package process;

import java.util.Random;

public class Box {
    private int milk;
    //定义一个成员变量,表示奶箱的状态
    private boolean state = false;

    //提供存储牛奶和获取牛奶的操作
    public synchronized void put(int milk) throws InterruptedException {

        //如果有,等待消费

        //如果没有就生产
        if(state)
        {
            wait();
        }
        this.milk = this.milk+milk;



        System.out.println("送奶工将" + milk + "瓶奶放入奶箱");
        //生产完毕之后,改变奶箱状态
        state = true;
        //唤醒其它等待线程
        notifyAll();
    }

    public synchronized void get() throws InterruptedException {
        Random random = new Random();
        int i = random.nextInt(5)+1;
        System.out.println("用户要取"+i+"瓶奶");
        //如果没有牛奶,等待生产
        if(!state)
        {
            wait();
        }
        //如果有,就消费
        if(i<=this.milk)
        {
            System.out.println("用户拿了" + i + "瓶奶");
            this.milk = this.milk-i;
            System.out.println("奶箱里还剩"+this.milk+"瓶牛奶");
        }
        else {
            System.out.println("牛奶不足");
            state = false;

        }

        notifyAll();
    }
}

package process;

public class BoxDemo {
    public static void main(String[] args) {
        Box b = new Box();
        Producer p = new Producer(b);
        Customer c = new Customer(b);
        Thread t1 = new Thread(p);
        Thread t2 = new Thread(c);
        t2.start();
        t1.start();
    }
}


程序做的不是很完善,但是基本模拟了这种情况。送入的奶是随机生成的,每次剩下的奶会与再次送入的奶相加。用户来取的话,如果奶不足,这个程序就设定不会提供了。线程调用加奶,随机一位用户来去,如果奶够的话就取走。如果你觉得逻辑不太好的,可以自己进行修改。

自己可以加入休眠,以及设定线程循环结束条件,或者修改主要逻辑。

就介绍这么多吧!之后有错误或补充的话,再改。

欢迎访问主页

更多推荐

Java从入门到精通十二(java线程)