【Java学习笔记九】多线程

程序:计算机指令的集合,它以文件的形式存储在磁盘上,是应用程序执行的蓝本。
进程:是一个程序在其自身的地址空间中的一次执行活动。进程是资源申请、调度和独立运行的单位,因此,它使用系统中的运行资源。而程序不能申请系统资源,不能被系统调度,也不能作为独立运行的单位,它不占用系统的运行资源。作为蓝本的程序可以被多次加载到系统的不同内存区域分别执行,形成不同的进程。基于进程的特点是允许计算机同时运行两个或更多的程序。
线程:是进程中的一个单一的顺序控制流程,一个进程在执行过程中,可以产生多个线程。每个线程也有自己产生、存在和消亡的过程。
线程又称为轻量级进程,它和进程一样拥有独立的执行控制,由操作系统负责调度,区别在于线程没有独立的存储空间,而是和所属进程中的其它线程共享一个存储空间。这使得线程间的通信远较进程简单,而进程之间的通信则比较困难,另外在资源的占用上,线程比进程要小。

线程(Thread)和进程(Process)的关系很密切,进程和线程是两个不同的概念,进程的范围大于线程

通俗的讲,进程就是一个程序,线程是这个程序能够同时做的各件事情。例如:媒体播放器运行时就是一个进程,而媒体播放器同时下载文件和播放歌曲就是两个线程。
从另一个角度讲,每个进程都拥有一组完整的属于自己的变量,而线程则共享一个进程中的这些数据。

主线程:程序启动时,一个线程立即运行,该线程称为程序的主线程。(main()方法),其他线程都是由主线程产生的,主线程通常必须最后完成执行,因此需要执行各种关闭动作。

观察以下程序:

  public class MainThreadDemo  {public static void main(String args[])	{Thread t = Thread.currentThread();System.out.println("当前线程名称是: " + t.getName());t.setName("MyJavaThread");System.out.println("改名后线程名称是: " + t.getName());System.out.println("输出当前线程: " + t);		}
}

代码分析:

  • Thread.currentThread()是一个静态方法,返回正在执行的线程对象的引用。
  • getName()方法可以得到当前引用线程的名称
  • setName(String s)可以改变线程的内部名称
  • 当将一个线程对象作为输出时,将输出[线程名称,优先级别,线程组名 ]
  • 每个线程都属于一个线程组,如果没有设定,则由JVM来设定。

并发编程:在计算机编程中有一个基本概念,就是在同一时刻处理多个任务的思想。许多程序设计问题都要求程序能够停下正在做的工作,转而处理某个其他问题,然后再返回主进程。多线程的任务相比传统的进程而言(只有一个主线程),可以同时有多个地方执行代码。

把问题切分成多个可独立运行的部分(任务),从而提高程序的响应能力。在程序中,这些彼此独立运行的部分称之为线程,上述概念被称为“并发”。

Java提供了类 java.lang.Thread 来方便多线程编程,这个类提供了大量的方法方便控制线程.

Thread类最重要的方法是run(),它为Thread类的方法start()所调用,为了指定我们自己的代码,需要覆盖run()方法,来提供我们线程所要执行的代码。

创建线程

  1. 继承java.lang.Thread()类,覆盖run()方法。在创建的Thread类的子类中重写run(),加入线程所要执行的代码。
class mythread extends Thread
{public void run(){}
}

例如:

package Test;class FileTransThread extends Thread{ private String fileName; public FileTransThread(String fileName){ this.fileName = fileName; } public void run(){ System.out.println("传送" + fileName); try{ Thread.sleep(1000 * 10); }catch(Exception ex){} System.out.println(fileName + "传送完毕"); } 
} 
public class Main { public static void main(String[] args) throws Exception { FileTransThread ft1 = new FileTransThread("文件1"); FileTransThread ft2 = new FileTransThread("文件2"); FileTransThread ft3 = new FileTransThread("文件3"); ft1.start(); System.out.println("1");System.out.println("2");ft2.start();System.out.println("3");System.out.println("4");ft3.start(); System.out.println("5");System.out.println("6");} 
} 	

运行结果:(两次完全相同的运行,但是结果却不同,说明线程大概是同步进行的,而且顺序是随机的)
在这里插入图片描述在这里插入图片描述

  1. 继承java.lang.Thread()类方法简单明了,符合大家的习惯。可是如果这个类已经继承了一个类,则没有办法再继承java.lang.Thread()类。这时我们可以实现java.lang.Runnable接口,并实现run()方法。然后通过这个类创建线程。详见实例:
class mythread implements Runnable  {public void run( )  {/* 实现该方法*/ }
}

例如:

package Test;class FileTransRunnable implements Runnable
{ private String fileName; public FileTransRunnable(String fileName){ this.fileName = fileName; }public void run(){ System.out.println("传送" + fileName); try{ Thread.sleep(1000 * 10); }catch(Exception ex){} System.out.println(fileName + "传送完毕"); } 
} 
public class Main
{ public static void main(String[] args) throws Exception { FileTransRunnable ft1 = new FileTransRunnable("文件1"); FileTransRunnable ft2 = new FileTransRunnable("文件2"); FileTransRunnable ft3 = new FileTransRunnable("文件3");Thread f1=new Thread(ft1);	//创建线程Thread f2=new Thread(ft2);Thread f3=new Thread(ft3);f1.start(); f2.start();f3.start();}
}

运行对象:
在这里插入图片描述

  • 对象可以自由地继承自另一个类。
  • 同一个runnable对象可以传递给多个线程,可以实现资源共享。

线程的启动

新建的线程不会自动开始运行,必须通过strat()方法启动线程。如果不调用这个方法,线程将不会运行。

线程的实现

线程从创建、启动到终止的整个过程叫做一个生命周期。线程总共由五个状态。

  1. 新建状态(new):创建之后还没有调用start()
  2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法,处于该状态的线程位于可运行线程池中,成为可运行,等待获取CPU的使用权。线程有资格运行但调度程序还没有把他选定为运行线程时线程所处的状态。当start()方法调用时,线程首先进入可运行状态。在线程运行之后或者从阻塞、等待或睡眠状态回来后,也返回到可运行状态。
  3. 运行状态(Running):线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态,这也是线程进入运行状态的唯一一种方式。就绪状态的线程获取了CPU,执行程序代码。
  4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
  • 等待阻塞:运行的线程执行wait()方法,该线程放入等待池中。
  • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,该线程放入锁池中。
  • 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
  1. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。这个线程对象也许还存在,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。 如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

简单来讲:

  1. 创建状态:使用new运算符创建一个线程。
  2. 可运行状态:使用start()方法启动一个线程后,系统分配了资源。
  3. 运行中状态:执行线程的run()方法。
  4. 阻塞状态:运行的线程因某种原因停止继续运行。
  5. 死亡状态:线程结束。

线程的调度

  1. 线程睡眠:public static void sleep(long millis) throws InterruptedException
    millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()方法的平台移植性较好。
  2. 线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法是Object类中的方法,行为等价于调用 wait(0) 一样。
  3. 线程让步:public static void yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。通过yield()方法,当前线程把cpu让给别的线程,而不用进入休眠状态而等待很长时间。该方法只影响当前正在运行的线程,且没有任何机制保证它将会被采纳。
  4. 线程加入:public final void join() throws InterruptedException 方法,在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
    另外,join()方法还有带超时限制的重载版本。 例如t.join(5000);则让线程等待5000毫秒,如果超过这个时间,则停止等待,变为可运行状态。
  5. 线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。

多线程的互斥和同步

线程的同步

在多线程的程序中,多个线程可能会对同一个资源并发访问。这种情况下,如果不对共享的资源进行保护,就可能产生问题。
所谓同步(synchronize),就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回,同时其它线程也不能调用这个方法。
通俗地讲,一个线程是否能够抢占CPU,必须考虑另一个线程中的某种条件,而不能随便让操作系统按照默认方式分配CPU,如果条件不具备,就应该等待另一个线程运行,直到条件具备。

同步的原理

Java中每个对象都有一个内置锁,当程序运行到非静态的synchronized同步方法上时,自动获得与正在执行代码类的当前实例(this实例)有关的锁。获得一个对象的锁也称为获取锁、锁定对象、在对象上锁定或在对象上同步。

一个对象只有一个锁。所以,如果一个线程获得该锁,就没有其他线程可以获得锁,直到第一个线程释放(或返回)锁。这也意味着任何其他线程都不能进入该对象上的synchronized方法或代码块,直到该锁被释放。释放锁是指持锁线程退出了synchronized同步方法或代码块。

同步的实现

在Java中通过互斥锁标志Synchronized关键字的运用来实现同步。Java中同步有两种方法:

  1. 方法级同步
 synchronized void method( ) {  //同步的方法}
  • 实现方法:在要标志为同步的方法前加上synchronized关键字。如:public synchronized void call(String msg){ }
  • 实现原理:当调用对象的同步方法时,线程取得对象锁或监视器,如果另一个线程试图执行同步方法,他就会发现被锁住了,就会进入挂起状态,直到对象监视器上的锁被释放为止。当锁住放啊的线程从方法中返回时,只有一个排队等候的线程可以访问对象。
  • 锁的作用域:该方法被执行的整个时间。
  1. 程序块级同步
synchronized()object
{}
  • 临界区:只希望防止多个线程同时访问方法内部的部分代码,而不是防止访问整个方法,通过这种方式分离出来的代码段被称为“临界区”,即需要进行互斥的代码段。
  • 实现方法:用synchronized来指定某个对象,此对象的锁被用来对花括号内的代码进行同步控制。如:
synchronized(target){target.call(msg);}
  • 实现原理:在进入同步代码前,必须得到object对象的锁,如果其他线程已经得到这个锁,那么就得等到锁被释放后才能进入临界区。
  • 锁的作用域:只在代码块运行的时间内

例子:

class TicketRunnable implements Runnable 
{ private int ticketNum = 3; //  以3 张票为例 public void run() { while (true) { String tName = Thread.currentThread().getName(); //  将需要独占 CPU的代码用 synchronized(this)包围起来 synchronized (this) { if (ticketNum <= 0) { System.out.println(tName + "无票"); break; } else { try { Thread.sleep(1000);//  程序休眠 1000 毫秒 }catch (Exception ex) {} ticketNum--; // 代码行1 System.out.println(tName + "卖出一张票,还剩" + ticketNum + "张票'); } } } }
} public class Main
{ public static void main(String[] args){ TicketRunnable tr = new TicketRunnable(); Thread th1 = new Thread(tr, "thread 1"); Thread th2 = new Thread(tr, "thread 2"); th1.start(); th2.start(); } 
} 

从以上代码可以看出,该方法的本质是将需要独占 CPU 的代码用synchronized(this)包围起来。如前所述,一个线程进入这段代码之后,就在 this 上加了一个标记,直到该线程将这段代码运行完毕,才释放这个标记。如果其他线程想要抢占 CPU,先要检查 this 上是否有这个标记。若有,就必须等待。

但是可以看出,该代码实际上运行较慢,因为一个线程的运行,必须等待另一个线程将同步代码段运行完毕。因此,从性能上讲,线程同步是非常耗费资源的一种操作。我们要尽量控制线程同步的代码段范围,理论上说,同步的代码段范围越小,段数越少越好,因此在某些情况下,推荐将小的同步代码段合并为大的同步代码段。

死锁

如果出现一种极端情况,一个线程等候另一个对象,而另一个对象又在等候下一个对象,以此类推。这个“等候链”如果进入封闭状态,也就是说,最后那个对象等候的是第一个对象,此时,所有线程都会陷入无休止的相互等待状态,造成死锁。尽管这种情况并非经常出现,但一旦碰到,程序的调试将变得异常艰难。
发生死锁必须同时满足的四个条件:

  1. 互斥条件。线程中使用的资源中至少要有一个是不能共享的。
  2. 至少有一个线程它必须持有一个资源且正在等待获取一个当前被别的线程持有的资源。
  3. 资源不能被线程抢占。
  4. 必须有循环等待,这时,一个线程等待其他线程所持有的资源,后者又在等待另一个线程所持有的资源。

死锁的实例:(待更,脑袋不转了)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/383800.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【C++11新特性】 C++11智能指针之weak_ptr

http://blog.csdn.net/xiejingfa/article/details/50772571 原创作品&#xff0c;转载请标明&#xff1a;http://blog.csdn.net/Xiejingfa/article/details/50772571 如题&#xff0c;我们今天要讲的是C11引入的三种智能指针中的最后一个&#xff1a;weak_ptr。在学习weak_ptr之…

【C++学习笔记四】运算符重载

当调用一个重载函数和重载运算符时&#xff0c;编译器通过把您所使用的参数类型和定义中的参数类型相比较&#xff0c;巨鼎选用最合适的定义。&#xff08;重载决策&#xff09; 重载运算符时带有特殊名称的函数&#xff0c;函数名是由关键字operator和其后要重载的运算符符号…

【C++11新特性】 C++11智能指针之unique_ptr

原创作品&#xff0c;转载请标明&#xff1a;http://blog.csdn.net/Xiejingfa/article/details/50759210 在前面一篇文章中&#xff0c;我们了解了C11中引入的智能指针之一shared_ptr&#xff0c;今天&#xff0c;我们来介绍一下另一种智能指针unique_ptr。 unique_ptr介绍 uni…

C++派生类对象和基类对象赋值

在C中&#xff0c;我们允许 将派生类对象赋给基类对象。&#xff08;不允许将基类对象赋给派生类对象&#xff09; 只会将基类对象成员赋值用基类指针指向派生类对象。&#xff08;不允许用派生类指针指向基类对象&#xff09; 基类指针只能操作基类中的成员基类引用作为派生类…

【C++11新特性】 C++11智能指针之shared_ptr

http://blog.csdn.net/Xiejingfa/article/details/50750037 原创作品&#xff0c;转载请标明&#xff1a;http://blog.csdn.net/Xiejingfa/article/details/50750037 C中的智能指针首先出现在“准”标准库boost中。随着使用的人越来越多&#xff0c;为了让开发人员更方便、更安…

C++(纯)虚函数重写时访问权限更改问题

我们知道在Java中是自动实现多态的&#xff0c;Java中规定重写的方法的访问权限不能缩小。那么在C中我们实现多态的时候是否可以更改&#xff08;缩小&#xff09;访问权限呢&#xff1f; 经过测试&#xff0c;得到的答案如下&#xff1a;如果用基类指针指向派生类对象实现多态…

C++ — 智能指针的简单实现以及循环引用问题

http://blog.csdn.net/dawn_sf/article/details/70168930 智能指针 ____________________________________________________ 今天我们来看一个高大上的东西&#xff0c;它叫智能指针。 哇这个名字听起来都智能的不得了&#xff0c;其实等你了解它你一定会有一点失望的。。。。因…

C++(静态)(常量)数据进行初始化问题以及静态变量析构

在C11标准以前我们都不可以在类中对数据成员初始化&#xff0c;仅能在构造函数中进行初始化&#xff1a; class A {int a,b; double c; string d;A():a(1),b(2),c(3),d(""){} };在C11标准以后我们可以在类中对非静态成员进行初始化。实际上的机制是在调用构造函数的…

C++this指针的用法

参考博客&#xff1a;https://www.cnblogs.com/zhengfa-af/p/8082959.html 在 访问对象的非静态成员时会隐式传递一个参数&#xff0c;即对象本身的指针&#xff0c;这个指针名为this。 例如&#xff1a; class A {int a1;public:A(){}void GetA(int a){cout<<this-&g…

C++开发者都应该使用的10个C++11特性

http://blog.jobbole.com/44015/ 感谢冯上&#xff08;治不好你我就不是兽医 &#xff09;的热心翻译。如果其他朋友也有不错的原创或译文&#xff0c;可以尝试推荐给伯乐在线。】 在C11新标准中&#xff0c;语言本身和标准库都增加了很多新内容&#xff0c;本文只涉及了一些皮…

C++不能被声明为虚函数

虚函数是为了实现多态&#xff0c;但是显然并不是所有函数都可以声明为虚函数的。 不能被声明为虚函数的函数有两类&#xff1a; 不能被继承的函数不能被重写的函数 因此&#xff0c;这些函数都不能被声明为虚函数 普通函数构造函数 如果构造函数定义为虚函数&#xff0c;则…

类的声明与定义

类的前向声明&#xff1a; class A;在声明之后&#xff0c;定义之前&#xff0c;类A是一个不完全类型&#xff0c;即知道A是一个类&#xff0c;但是不知道包含哪些成员。不完全类型只能以有限方式使用&#xff0c;不能定义该类型的对象&#xff0c;不完全类型只能用于定义指向…

shared_ptr的一些尴尬

http://blog.csdn.net/henan_lujun/article/details/8984543 shared_ptr在boost库中已经有多年了&#xff0c;C11又为其正名&#xff0c;把他引入了STL库&#xff0c;放到了std的下面&#xff0c;可见其颇有用武之地&#xff1b;但是shared_ptr是万能的吗&#xff1f;有没有什…

C++转换构造函数和类型转换函数

参考博客&#xff1a;https://blog.csdn.net/feiyanaffection/article/details/79183340 隐式类型转换 如果不同类型的数据在一起操作的时候编译器会自动进行一个数据类型转换。例如常用的基本数据类型有如下类型转换关系&#xff1a; 转换构造函数 构造函数有且仅有一个参数…

C++总结8——shared_ptr和weak_ptr智能指针

http://blog.csdn.net/wendy_keeping/article/details/75268687 智能指针的提出&#xff1a;智能指针是存储指向动态分配对象指针的类&#xff0c;用于生存期控制。能够确保正确销毁动态分配的内存&#xff0c;防止内存泄露。 1.智能指针的分类&#xff1a; 不带引用计数的智能…

C++析构函数执行顺序

今天发现主程序中有多个对象时析构函数的执行顺序不是对象定义的顺序&#xff0c;而是对象定义顺序反过来。 思考了一下&#xff0c;结合之前继承、成员对象等的析构函数执行的顺序&#xff0c;我觉得析构函数执行的顺序为&#xff1a;构造函数的顺序反过来&#xff0c;可能是…

c++写时拷贝1

http://blog.csdn.net/SuLiJuan66/article/details/48882303 Copy On Write Copy On Write(写时复制)使用了“引用计数”&#xff08;reference counting&#xff09;&#xff0c;会有一个变量用于保存引用的数量。当第一个类构造时&#xff0c;string的构造函数会根据传入的参…

【C++学习笔记五】模板

模板是泛型编程的基础 函数模板 模板定义以关键字template开始&#xff0c;后跟一个模板参数列表。这是一个逗号分隔的一个或多个模板参数的列表。用尖括号包围起来。 模板函数定义的一般形式&#xff1a; template <class type> ret-tye func-name(parameter list) …

【Java学习笔记十】输入输出流

在Java.io包中提供了一系列用于处理输入/输出的流类。从功能上分为两类&#xff1a;输入流和输出流。从六结构上可分为&#xff1a;字节流&#xff08;以字节为处理单位&#xff09;和字符流&#xff08;以字符为处理单位&#xff09;。 字符是由字节组成。在Java中所有字符用…

C++ 写时拷贝 2

什么情况下会用到c中的拷贝构造函数】&#xff1a; 1&#xff09;用已经存在的同类的对象去构造出另一个新的对象 2&#xff09;当函数的形参是类的对象时&#xff0c;这时调用此函数&#xff0c;使用的是值的拷贝&#xff0c;也会调用拷贝构造函数 3&#xff09;当函数的返…