在前面我们介绍的一些内容中,我们的程序都是一条执行流,一步一步的执行。但其实这种程序对我们计算机的资源的使用上是低效的。例如:我们有一个用于计算的程序,主程序计算数据,在计算的过程中每得到一个结果就需要将其保存到外部磁盘上,那么难道我们的主程序每次都要停止等待CPU将结果保存到磁盘之后,再继续完成计算工作吗?要知道磁盘的速度可是巨慢的(相对内存而言),我们如果能分一个线程去完成磁盘的写入工作,主线程还是继续计算的话,是不是效率更高了呢?其实,并发就是这样的一种思想,使用时间片分发给各个线程CPU的使用时间,给人感觉好像程序在同时做多个事情一样,这样做的好处主要在于它能够对我们整个的计算机资源有一个充分的利用,在多个线程竞争计算机资源不冲突的前提下,充分的利用我们的资源。本篇文章首先来介绍并发的最基本的内容-----线程。主要涉及以下一些内容:

  • 定义线程的两种不同的方法及它们之间的区别
  • 线程的几种不同的状态及其区别
  • Thread类中的一些线程属性和方法
  • 多线程遇到的几个典型的问题

     一、创建一个线程

     首先我们看创建一个线程的第一种方式,继承Thread类并重写其run方法。

public class MyThread extends Thread {
@Override
public void run(){
System.out.println("this is mythread");
}
}

现在我们来看看在主程序中如何启动我们自定义的线程:

public static void main(String[] args) {
Thread myThread = new MyThread();
myThread.start();
}

我们首先构建一个Thread实例,调用其start方法,调用该方法会为线程分配其所必须的堆栈资源,计数器,时间片等,并在该方法的结束时刻调用我们重写的run方法,完成线程的启动。

但是在Java中类是单继承的,也就是如果某个类已经有了父类,那么它就不能被定义成线程类。当然,Java中也提供了第二种方法来定义一个线程类,这种方式实际上更加的接近本质一些。通过继承接口Runnable并在其内部重写一个run方法。

public class MyThread implements Runnable{
@Override
public void run(){
System.out.println("this is mythread");
}
}

启动线程的方式和上一种略微有点不同,但是本质上都是一样的。

public static void main(String[] args) {
Thread myThread = new Thread(new MyThread());
myThread.start();
}

这里我们利用Thread的一个构造函数,传入一个实现了Runnable接口的参数。下面我们看看这个构造函数的具体实现:

public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}

调用init方法对线程的一些状态优先级等做一个初始化的操作,我们顺便看看使用第一种方式创建线程实例的那个无参的构造函数:

public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}

可以看到,两个构造函数的内部调用的是同一个方法,只是传入的参数不同而已。所以他们之间的区别就在于初始化的时候这个Runnable参数是否为空,当然这个参数的用处在run方法中也可以看出来:

@Override
public void run() {
if (target != null) {
target.run();
}
}

如果我们使用第二种方式构建Thread实例,那么此处的target肯定不会是null,自然会调用我们重写的run方法。如果使用的是第一种方式构建的Thread实例,那么就不会调用上述的run方法,而是调用的我们重写的Thread的run方法,所以从本质上看,两种方式的底层处理都是一样的。

这就是创建一个线程类并启动该线程的两种不同的方式,表面上略有不同,但是实际上都是一样的调用init方法完成初始化。对于启动线程的start方法的源码,由于调用本地native方法,暂时并不易解释,有兴趣的可以使用jvm指令查看本地方法的实现以了解整个线程从分配资源到调用run方法启动的全过程。

     二、线程的多种状态

     线程是有状态的,它会因为得不到锁而阻塞处于BLOCKED状态,会因为条件不足而等待处于WAITING状态等。Thread中有一个枚举类型囊括了所有的线程状态:

public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}

NEW状态表示线程刚刚被定义,还未实际获得资源以启动,也就是还未调用start方法。

RUNNABLE表示线程当前处于运行状态,当然也有可能由于时间片使用完了而等待CPU重新的调度。

BLOCKED表示线程在竞争某个锁失败时被置于阻塞状态

WAITING和TIMED_WAITING表示线程在运行中由于缺少某个条件而不得不被置于条件等待队列等待需要的条件或资源。

TERMINATED表示线程运行结束,当线程的run方法结束之后,该线程就会是TERMINATED状态。

我们可以调用Thread的getState方法返回当前线程的状态:

/*定义一个线程类*/
public class MyThread implements Runnable{
@Override
public void run(){
System.out.println("myThread's state is : "+Thread.currentThread().getState());
}
}
/*启动线程*/
public static void main(String[] args) throws InterruptedException {
Thread myThread = new Thread(new MyThread());
myThread.start();
Thread.sleep(1000);
System.out.println("myThread's state is : "+myThread.getState());
}

我们两次输出myThread线程的当前状态,在run方法中输出结果显示该线程状态为RUNNABLE,当该run方法执行结束时候,我们又一次输出该线程的当前状态,结果显示该线程处于TERMINATED。至于更加复杂的线程状态,我们将在后续的文章中逐渐进行介绍。

     三、Thread类中的其他一些常用属性及方法

     以上我们介绍了创建线程的两种不同的方式以及线程的几种不同状态,有关于线程信息属性的一些方法还没有介绍。本小节将来简单介绍下线程所具有的基本的一些属性以及一些常用的方法。

首先每个线程都有一个id和一个name属性,id是一个递增的整数,每创建一个线程该id就会加一,该id的初始值是10,每创建一个线程就会往上加一。所以该id也间接的告诉了我们当前线程在所有线程中的位置。name属性往往是以“Thread-”+编号作为某个具体线程的name值。例如:

public static void main(String[] args){
for (int i=0;i<10;i++){
Thread myThread = new Thread(new MyThread());
myThread.start();
System.out.println(myThread.getName());
}
}

输出结果:

除此之外,Thread中还有一个属性daemon,它是一个boolean类型的变量,该变量指示了当前线程是否是一个守护线程。守护线程主要用于辅助主线程完成工作,如果主线程执行结束,那么它的守护线程也会跟着结束。例如:我们的main程序在执行的时候,始终有一个垃圾回收线程作为守护线程辅助一些对象的回收工作,当main程序执行结束时,守护线程也将退出内存。关于守护线程有几个方法:

public final boolean isDaemon() :判断当前线程是否是守护线程

public final void setDaemon(boolean on):设置当前线程是否作为守护线程

还有一个方法较为常见,join。该方法可以让一个线程等待另一个线程执行结束之后再继续工作。例如:

public class MyThread implements Runnable{
@Override
public void run(){
System.out.println("myThread is running");
}
}
public static void main(String[] args) {
Thread myThread = new Thread(new MyThread());
myThread.start(); //主线程等待myThread线程执行结束
myThread.join(); System.out.println("waiting myThread done....");
}

输出结果:

有人可能会疑问,我们使用多线程不就是为了充分利用计算机资源,使其同时执行多个任务,为什么又要让一个线程等待另一个线程呢?其实某些时候,主线程需要拿到所有分支线程计算的结果再一次进行计算,各个分支线程的进度各有快慢,主线程唯有等待他们全部执行结束之后才能继续。此时就需要使用join方法了,所以说每一个方法的存在都有其可应用的场景。至于这个join的源代码也是很有研究价值的,我们将在后续的文章中对其源代码的实现进行进一步的学习。

还有一些属性和方法,限于篇幅,本文不再继续学习,大家可以自行查看源码进行学习。下面我们看看多线程之后可能会遇到的几个经典的问题。

     四、多线程遇到的几个典型的问题

     第一个可能遇到的问题是,竞态条件。也就是说,当多个线程同时访问操作同一个对象的时候,最终的结果可能正确也可能不正确,具体的执行情况和线程实际的执行时序有关。

例如:

/*我们定义一个线程*/
public class MyThread implements Runnable{
public static int count; @Override
public void run(){
try {
Thread.currentThread().sleep((int)(Math.random()*100));
count++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/*main方法中启动多个线程*/
public static void main(String[] args){ Thread[] threads = new Thread[100];
for (int i=0;i<100;i++){
threads[i] = new Thread(new MyThread());
threads[i].start();
} for (int j =0;j<100;j++){
threads[j].join();
} System.out.println(MyThread.count);
}

首先在我们自定义的线程类中,有一个static公共变量,而我们的run方法主要就做两个事情,随机睡一会和count增一。再来看main函数,首先定义了一百个线程并逐个启动,然后主线程等待所有的子线程完成之后输出count的值。

按照我们一般的思维,这一百个线程,每个线程都是为count加一,最后的输出结果应该是100才对。但是实际上我们多次运行该程序得到的结果都是不一样的,但几乎都是小于100的。





为什么会出现这样的情况呢?主要原因还是在于为count加一这个操作,它并非是原子操作,也就是说想要为count加一需要经过起码两个步骤:

  • 取count的当前值
  • 为count加一

因为每个线程都是随机睡了一会,有可能两个线程同时醒来,都获取到当前的count的值,又同时为其加一,这样就导致两个不同的线程却只为count增加了一次值。这种情况在多线程的前提下,发生的概率就更大了,所以这也是为什么我们得到的结果始终小于100但又每次都不同的原因。

第二个问题是,内存的可见性问题。就是说,如果两个线程共享了同一个参数,其中一个线程对共享参数的修改而另一个线程并不会立马能够看到。原因是这些修改会被暂存在CPU缓存中,而没有立马写回内存。例如:

public class MyThread extends Thread{
public static boolean flag = false; @Override
public void run(){
while(!flag){
//just running
}
System.out.println("my thread has finished ");
}
}
public static void main(String[] args) throws InterruptedException {
Thread myThread = new MyThread();
myThread.start(); Thread.sleep(1000);
MyThread.flag = true;
System.out.println("main thread has finished");
}

首先我们定义一个线程类,该线程类中有一个静态共享变量flag,run方法做的事情很简单,死循环的做一些事情,等待外部线程更改flag的值,使其退出循环。而main方法首先启动一个线程,然后修改共享变量flag的值,按照常理线程myThread在main线程修改flag变量的值之后将退出循环,打印退出信息。但是实际的输出结果为:

main线程已经结束了,而整个程序并没有结束,线程myThread的结束信息也没有被打印,这就说明myThread线程还困在while循环中,但是实际上主线程已经将flag的值修改了,只是myThread无法看见。这是什么原因呢?

我们知道,每个线程都有一些缓存,往往为了效率,对一个变量值的修改并不会立马写会内存,而是注入缓存中,等到一定的时候才写回内存,而当别的线程来修改这些共享的变量的时候,他们是从内存进行读取的,修改后可能也没有及时的写回内存中,这就很容易导致其他线程根本就看不到你所做的修改。这就是典型的内存可见性问题。

本小节简单的介绍了多线程的两个典型的问题,解决办法其实有多种,我们将在下篇文章中涉及。

以上的本篇内容主要介绍了线程的基本概念,如何创建一个线程,如何启动一个线程,还有与线程相关的一些基本的属性和方法,总结不到之处,望大家指出,相互学习。下篇文章将介绍一个用于解决多线程并发问题的关键字synchronized。

Java并发之线程的更多相关文章

  1. Java并发之线程中断

    前面的几篇文章主要介绍了线程的一些最基本的概念,包括线程的间的冲突及其解决办法,以及线程间的协作机制.本篇主要来学习下Java中对线程中断机制的实现.在我们的程序中经常会有一些不达到目的不会退出的线程 ...

  2. Java并发之线程管理(线程基础知识)

    因为书中涵盖的知识点比较全,所以就以书中的目录来学习和记录.当然,学习书中知识的时候自己的思考和实践是最重要的.说到线程,脑子里大概知道是个什么东西,但很多东西都还是懵懵懂懂,这是最可怕的.所以想着细 ...

  3. Java并发之线程转储

    一.java线程转储 java的线程转储可以被定义为JVM中在某一个给定的时刻运行的所有线程的快照.一个线程转储可能包含一个单独的线程或者多个线程.在多线程环境中,比如J2EE应用服务器,将会有许多线 ...

  4. java并发之线程池的使用

    背景 当系统并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要消耗大量的系统资源. 所以需要一个办法使得线程可以 ...

  5. Java并发之——线程池

    一. 线程池介绍 1.1 简介 线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务.线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡 ...

  6. Java 并发之线程安全

    写线程安全的代码,说白了就是管理一个类的共享的.可变的状态.只要有多于 1 个线程对类的状态进行写入,那么就必须用同步来协调这多个线程对状态的访问.对于一个没有状态的类来说(简单的理解就是只有方法没有 ...

  7. Java并发之线程异常捕获

    由于线程的本质特性,使得你不能捕获从线程中逃逸的异常,如: import java.util.concurrent.ExecutorService; import java.util.concurre ...

  8. Java并发之线程间的协作

    上篇文章我们介绍了synchronized关键字,使用它可以有效的解决我们多线程所带来的一些常见问题.例如:竞态条件,内存可见性等.并且,我们也说明了该关键字主要是一个加锁和释放锁的集成,所有为能获得 ...

  9. java并发之线程同步(synchronized和锁机制)

    使用synchronized实现同步方法 使用非依赖属性实现同步 在同步块中使用条件(wait(),notify(),notifyAll()) 使用锁实现同步 使用读写锁实现同步数据访问 修改锁的公平 ...

随机推荐

  1. Classy(排序)

    Description In his memoir So, Anyway. . ., comedian John Cleese writes of the class di erence betwee ...

  2. 万能头文件#include

    #include<bits/stdc++.h>包含了目前c++所包含的所有头文件!!!! 测试结果POJ不支持HDU,NYOJ支持

  3. HPU--1141 蜗牛爬树

    1141: 蜗牛爬树 [模拟] 时间限制: 1 Sec 内存限制: 128 MB提交: 377 解决: 60 统计 题目描述 阿门阿前一棵葡萄树,阿嫩阿嫩绿地刚发芽,蜗牛背著那重重的壳呀,一步一步地往 ...

  4. 弱校ACM奋斗史

    看到这篇文章, 已是大三了, 我的ACM之路也即将走向终点, 感慨自己还是不够努力, 给自己的大学留下诸多遗憾. 和他们相比, 我差的就是太远了, 值得高兴的是我们学校有一个好老师-----赵靖老师, ...

  5. 【原创】IE8升级到IE11控制台报错的解决方案

    公司win7 64位 英文版系统,刚从IE8升级到IE11,在我F12准备调试js的时候,竟然发现控制台报错了!天啦撸,顿时慌了有木有! 网上搜索了半天,解决方案如下: http://www.micr ...

  6. php 下载文件

    <?php header("Content-type:text/html;charset=utf-8"); // $file_name="cookie.jpg&qu ...

  7. InnoDB online DDL与快速索引创建

    导读:在MySQL5.6之前版本,Innodb表的许多DDL操作是非常昂贵.许多ALTER TABLE操作的原理是通过创建新的空表,定义被要求的表选项和索引,然后逐行拷贝已存在记录到新表,在插入行时更 ...

  8. Oracle插入多个值的问题

    首先我们建立表T123 SQL),age int); 然后试图向表中像DB2一样插入多个value,却得到了错误的信息. SQL),(); ),() * ERROR at line : ORA: SQ ...

  9. 设计模式之桥接模式(Bridge模式)

    我想大家小时候都有用蜡笔画画的经历吧.红红绿绿的蜡笔一大盒,根据想象描绘出格式图样.而毛笔下的国画更是工笔写意,各展风采.而今天我们的故事从蜡笔与毛笔说起. 设想要绘制一幅图画,蓝天.白云.绿树.小鸟 ...

  10. Fortran与C混合编程(转自Ubuntu)

    Fortran与C混合编程 由于 GNU 的 Fortran 和 C 语言二者的函数彼此可以直接相互调用,所以混合编程可以非常容易地实现.只要你足够仔细,确保函数调用时传递的参数类型正确,函数就可以在 ...