在介绍线程之前,先理解一下"程序"的概念:
程序:即为一条条的按照语法规则排列起来的指令序列。
程序默认的是保存在外存(硬盘)中,程序在运行的时候,被操作系统装载到内存中,然后CPU从内存中读取并执行。当一个程序被装载到内存,并且被CPU执行的时候,这是这个程序就变成了一个正在执行的程序,也就是所谓的进程。
那我们感到有一点奇怪,我的计算机只是8核的,但是进程数远远多于8个,那操作系统是如何做到多进程同时进行的呢?原来是将时间进行切分,在不同的时间片断内执行不同的进程,这样多个进程就可以交替执行,看上去仿佛是同时进行,如下:
如上,A和B两个进程是交替执行的,观察A的执行情况是:运行一段时间,再停留一段时间,在继续运行一段时间,如此往复运行。
那么,什么又是线程呢?即为在一个进程中同时执行的多个操作,称为线程;
那进程和线程有什么区别呢?
1.进程是可以单独执行的,而线程是不能单独执行的;
2.操作系统会为每个进程分配内存空间,但是不会为线程分配空间;
3.线程必须运行在进程之内,换言之:没有进程,就不会有其内线程;
4.线程实际上是进程中的一段代码,只不过这段代码可以独立的进行;
那操作系统又是如何做到多线程同时进行的呢?和多进程同时进行同样的道理:将所在的进程时间单位切片,在不同的时间片段内执行不同的线程。
以下是Java程序运行的简略图:
由上图可以看出,我们写的或运行的程序都是一个个线程,都是需要强烈依赖JVM虚拟机这个进程才能工作,离开虚拟机是不能工作的,所以Java是线程级别的,不像C和C++写出来的都是".exe"文件,是可以直接运行的程序,他们是进程级别的。
那么如何创建线程呢?第一种方法是:将其声明为Thread的子类,该子类重写run方法,接下来分配并启动该子类的实例。
package com.Jevin.thread.demo3;
public class ThreadTest1 extends Thread {
/**
* 我们把要执行的操作写在run()方法里面,run()方法又被称为线程体;
*/
@Override
public void run() {
for(int i=0;i<1000000;i++){
System.out.println("小黑线程在计数:i="+i);
}
}
}
package com.Jevin.thread.demo3;
public class ThreadTest2 extends Thread {
/**
* 我们把要执行的操作写在run()方法里面,run()方法又被称为线程体;
*/
@Override
public void run() {
for(int i=0;i<1000000;i++){
System.err.println("小红在计数:i="+i);
}
}
}
package com.Jevin.thread.demo3;
public class ThreadMain {
public static void main(String[] args) {
//创建线程对象:
ThreadTest1 t1 = new ThreadTest1();
ThreadTest2 t2 = new ThreadTest2();
/**
* 线程启动正确的方式:
* 启动线程的时候,要调用线程的start()方法,线程启动之后会自动执行线程体(也就是run()方法)
*/
t1.start();
t2.start();
/**
* 以下是错误的方式:如果用引用直接去调用run()方法,
* 这不是启动线程,而是调用方法,这样线程会失去并发性
*/
//t1.run();
//t2.run();
/**
* 除了t1和t2两个线程之外,还有虚拟机创建的主线程,用来执行main()方法
*/
for(int i=0;i<300000;i++){
System.out.println("====================主线程在运行");
}
}
}
上面的线程名称"小黑线程"和“小红线程”都是写死的,如何写活呢?请看一下事例:
package com.Jevin.thread.demo3;
public class ThreadTest1 extends Thread {
public ThreadTest1(String name){
super(name);
}
/**
* 我们把要执行的操作写在run()方法里面,run()方法又被称为线程体;
*/
@Override
public void run() {
for(int i=0;i<1000000;i++){
System.out.println(this.getName()+"在计数:i="+i);
}
}
}
package com.Jevin.thread.demo3;
public class ThreadTest2 extends Thread {
public ThreadTest2(String name){
super(name);
}
/**
* 我们把要执行的操作写在run()方法里面,run()方法又被称为线程体;
*/
@Override
public void run() {
for(int i=0;i<1000000;i++){
System.err.println(this.getName()+"在计数:i="+i);
}
}
}
package com.Jevin.thread.demo3;
public class ThreadMain {
public static void main(String[] args) {
//创建线程对象:
ThreadTest1 t1 = new ThreadTest1("小黑线程");
ThreadTest2 t2 = new ThreadTest2("小红线程");
/**
* 线程启动正确的方式:
* 启动线程的时候,要调用线程的start()方法,线程启动之后会自动执行线程体(也就是run()方法)
*/
t1.start();
t2.start();
/**
* 以下是错误的方式:如果用引用直接去调用run()方法,
* 这不是启动线程,而是调用方法,这样线程会失去并发性
*/
//t1.run();
//t2.run();
/**
* 除了t1和t2两个线程之外,还有虚拟机创建的主线程,用来执行main()方法
*/
for(int i=0;i<300000;i++){
System.out.println("====================主线程在运行");
}
}
}
创建线程的第二种方式是:实现Runnable接口,并重写run()方法,然后分配该类的实例,在创建Thread时作为一个参数来传递并启动。
package com.Jevin.thread.demo4;
/**
* 我们把实现Runnable接口的类称为目标对象;
*/
public class Target1 implements Runnable {
@Override
public void run() {
for(int i=0;i<1000000;i++){
System.out.println("小黑目标线程正在运行:i="+i);
}
}
}
package com.Jevin.thread.demo4;
public class Target2 implements Runnable{
@Override
public void run() {
for(int i=0;i<1000000;i++){
System.err.println("小红目标线程正在运行:i="+i);
}
}
}
package com.Jevin.thread.demo4;
public class TargetMain {
public static void main(String[] args) {
//创建目标对象:
Target1 target1 = new Target1();
Target2 target2 = new Target2();
//创建线程对象,并让线程对象执行指定的目标对象:
Thread t1 = new Thread(target1); //t1线程对象执行target1目标对象
Thread t2 = new Thread(target2); //t2线程对象执行target2目标对象
//启动线程对象:
t1.start();
t2.start();
}
}
上述线程名称写活的方式如下:
package com.Jevin.thread.demo4;
/**
* 我们把实现Runnable接口的类称为目标对象;
*/
public class Target1 implements Runnable {
@Override
public void run() {
for(int i=0;i<1000000;i++){
System.out.println(Thread.currentThread().getName()+"正在运行:i="+i);
}
}
}
package com.Jevin.thread.demo4;
public class Target2 implements Runnable{
@Override
public void run() {
for(int i=0;i<1000000;i++){
System.err.println(Thread.currentThread().getName()+"正在运行:i="+i);
}
}
}
package com.Jevin.thread.demo4;
public class TargetMain {
public static void main(String[] args) {
//创建目标对象:
Target1 target1 = new Target1();
Target2 target2 = new Target2();
//创建线程对象,并让线程对象执行指定的目标对象:
Thread t1 = new Thread(target1,"小黑目标线程"); //t1线程对象执行target1目标对象
Thread t2 = new Thread(target2,"小红目标线程"); //t2线程对象执行target2目标对象
//启动线程对象:
t1.start();
t2.start();
}
}
下面介绍一下“用户线程”和“守护线程”;
用户线程:只要线程没有运行结束,JVM是不会主动停止这个线程的,这种线程称为用户线程;
守护线程:当JVM发现只有守护线程在运行的时候,JVM会主动的关闭守护线程,再关闭JVM;
先用代码演示用户线程:
package com.Jevin.thread.demo5;
/**
* 用户线程,只要线程没有运行结束,JVM是不会主动停止这个线程的,这种线程称为用户线程
* 换句话说,只要用户线程没有结束,JVM是不会关闭的;
* 实际开发中,都是用的是用户线程
*/
public class UserThread extends Thread{
public UserThread(String name){
super(name);
}
/**
* 我们把要执行的操作写在run()方法里面,run()方法又被称为线程体;
*/
@Override
public void run() {
for(int i=0;i<10000000;i++){
System.out.println(this.getName()+"在计数:i="+i);
}
}
}
package com.Jevin.thread.demo5;
public class UserThreadMain {
public static void main(String[] args) {
UserThread t = new UserThread("用户线程");
t.start();
}
}
这里有个小玩意,就是线程启动的start()方法,可以放在构造器中的,如下:
package com.Jevin.thread.demo5;
/**
* 用户线程,只要线程没有运行结束,JVM是不会主动停止这个线程的,这种线程称为用户线程
* 换句话说,只要用户线程没有结束,JVM是不会关闭的;
* 实际开发中,都是用的是用户线程
*/
public class UserThread extends Thread{
public UserThread(String name){
super(name);
start();
}
/**
* 我们把要执行的操作写在run()方法里面,run()方法又被称为线程体;
*/
@Override
public void run() {
for(int i=0;i<10000000;i++){
System.out.println(this.getName()+"在计数:i="+i);
}
}
}
package com.Jevin.thread.demo5;
public class UserThreadMain {
public static void main(String[] args) {
UserThread t = new UserThread("用户线程");
}
}
下面介绍一下守护线程:
package com.Jevin.thread.demo5;
/**
* 当JVM发现只有守护线程在运行的时候,JVM会主动的关闭守护线程,再关闭JVM
* 换言之,JVM是不会让守护线程一直运行下去的
* 表现为:在控制台,有时候没有任何东西打印输出,有时候有一个,有时候有多个
*/
public class DaemonThread extends Thread{
public DaemonThread(String name){
super(name);
}
/**
* 我们把要执行的操作写在run()方法里面,run()方法又被称为线程体;
*/
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println(this.getName()+"在计数:i="+i);
}
}
}
package com.Jevin.thread.demo5;
public class DaemonThreadMain {
public static void main(String[] args) {
//线程对象创建好之后,默认是用户线程
DaemonThread t = new DaemonThread("守护线程");
//将一个线程标记为守护线程:
//注意:将一个线程标记为守护线程,一定要在线程启动之前,否则,会出现IllegalThreadStateException异常
t.setDaemon(true);
t.start();
}
}
那么什么时候需要使用”线程”呢?即为当多个操作需要同时执行的时候,必须要使用多线程。
之前写的有个切分文件进行拷贝的博客:https://mp.csdn.net/postedit/83502214
这里我们用多线程实现一下:
package com.Jevin.thread.demo6;
import java.io.File;
import java.text.DecimalFormat;
/**
* 文件拷贝包工头
*/
public class FileCopyContractor {
private File srcFile; //源文件
private int splitCount; //文件切分的份数
/**
* 监工需要知道的条件:
*/
private String desFile; //目标文件
private String tempFile; //拷贝过程中的临时文件
private long fileSize; //文件总的大小
private FileCopyWorkerThread[] arr; //所有保存工人线程数据的数组
public FileCopyContractor() {}
public FileCopyContractor(File srcFile, String desPath, int splitCount) {
super();
this.srcFile = srcFile;
this.splitCount = splitCount;
//组织目标文件:
String fileName = srcFile.getName();
this.desFile = desPath + File.separator + fileName;
this.tempFile = desFile + ".td";
}
public FileCopyContractor(String srcFile, String desPath, int splitCount) {
this(new File(srcFile), desPath, splitCount);
}
/**
* 包工头开始工作:
*/
public void assignWork() {
//获取原文件的大小:
fileSize = srcFile.length();
System.out.println("文件的大小是:" + fileSize);
//根据切分的份数和源文件的大小,计算每个工人的平均工作量
long perWorkerSize = fileSize / this.splitCount;
/**
* 创建数组对象
*/
arr = new FileCopyWorkerThread[this.splitCount];
//计算第一个工人的开始位置和结束位置:
long startPost = 0L;
long endPost = perWorkerSize;
//包工头创建多个工人:
for (int i = 0; i < this.splitCount; i++) {
//创建工人对象:
FileCopyWorkerThread fileCopyWorkerThread = new FileCopyWorkerThread("工人-" + i, srcFile, tempFile, startPost, endPost);
/**
*启动工人线程,工人开始工作:
*/
fileCopyWorkerThread.start();
/**
* 把工人保存到数组中
*/
arr[i] = fileCopyWorkerThread;
//包工头计算下一个工人的开始位置和结束位置:
startPost = endPost;
endPost = startPost + perWorkerSize;
//如果是最后一个工人,则做到最后;
if (i == this.splitCount - 2) {
endPost = fileSize;
}
}
/**
* 当所有的工人创建好之后,创建监工线程
* 这里的CPU使用率将会急速上升,达到80%
*/
new MonitorThread();
}
/**
* 监工线程的任务:(1)统计总的文件拷贝进度;(2)当拷贝完成之后,将临时文件名称改为目标文件名称
* <p>
* 监工线程需要知道的条件: 1.目标文件的名称 2.临时文件的名称 3.文件总的大小 4.所有线程的拷贝总量: (1)获取所有工人线程对象,调取工人线程对象上的getCopyedSize(),加在一起就是拷贝总量
* (2)包工头将工人信息保存到一个数组中,然后把数组传递给监工
*/
class MonitorThread extends Thread {
public MonitorThread(){
start();
}
@Override
public void run() {
DecimalFormat df = new DecimalFormat("##.0%");
//当拷贝没有完成的时候,监工就要一直工作:
while (!copyIsOver()) {
long total = totalCopySize(); //取得所有工人的拷贝总量
if(total == fileSize){
break;
}
double d = total / (double) fileSize;
String str = df.format(d);
System.out.println("拷贝进度"+str);
}
try {
//确保所有的工人线程把流关闭之后,再去给文件改名
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//所有工人完成工作之后,把临时文件名称改成目标文件名称:
File file1 = new File(tempFile);
File file2 = new File(desFile);
if(file1.renameTo(file2)){
System.out.println(srcFile + "拷贝到"+desFile+"完成,进度100%");
}else{
System.out.println("重命名文件失败");
}
}
/**
* 取得所有工人的拷贝总量
*
* @return
*/
private long totalCopySize() {
long total = 0L;
for (FileCopyWorkerThread work : arr) {
total += work.getCopyedSize();
}
return total;
}
/**
* 判断拷贝操作是否已经完成 当所有的工人都已经完成的时候,就说明拷贝完成了 只要有一个工人拷贝未完成,则说明整个拷贝工作未完成
*
* @return
*/
private boolean copyIsOver() {
for (FileCopyWorkerThread work : arr) {
if (work.isAlive()) {
return false;
}
}
return true;
}
}
}
package com.Jevin.thread.demo6;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
/**
* 文件拷贝工人
*/
public class FileCopyWorkerThread extends Thread {
private String name; //工人名称
private File srcFile; //源文件
private String desFile; //目标文件
private long startPost; //开始位置
private long endPost; //结束位置
private long copyedPost; //已经拷贝的位置
public FileCopyWorkerThread(String name, File srcFile, String desFile, long startPost, long endPost) {
super();
this.name = name;
this.srcFile = srcFile;
this.desFile = desFile;
this.startPost = startPost;
this.endPost = endPost;
this.copyedPost = this.startPost; //初始化拷贝位置即为初始位置
//System.out.println(name+"[开始位置是:"+this.startPost+",结束位置是:"+this.endPost+"]");
}
/**
* 取得工人已经拷贝的数量
* @return
*/
public long getCopyedSize(){
return this.copyedPost - this.startPost;
}
/**
* 工人开始工作
*/
@Override
public void run(){
RandomAccessFile rin = null; //读数据流
RandomAccessFile rout = null; //写数据流
try {
rin = new RandomAccessFile(this.srcFile,"r");
rout = new RandomAccessFile(this.desFile,"rw");
//定位读写的位置:
rin.seek(this.startPost); //开始读的位置
rout.seek(this.startPost); //开始写的位置
byte[] b = new byte[1024*1024];
int i = 0;
//当已经拷贝的位置小于结束位置,并且未拷贝到文件结尾,就一直循环拷贝下去:
while((this.copyedPost < this.endPost) && (i=rin.read(b)) != -1){
if((this.copyedPost + i) > this.endPost){
i = (int) (this.endPost - this.copyedPost);
}
rout.write(b,0,i);
this.copyedPost += i;
//System.out.println(name+"正在工作,已经拷贝的位置是:"+this.copyedPost+",结束位置是:"+this.endPost);
}
//System.out.println(name+"结束工作,已经拷贝的位置是:"+this.copyedPost+",结束位置是:"+this.endPost);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
if(rin != null){
rin.close();
}
if(rout != null){
rout.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
package com.Jevin.thread.demo6;
public class MainTest {
public static void main(String[] args){
String file = "D:\\tools\\mysql-8.0.12-winx64.zip";
FileCopyContractor fileCopyContractor = new FileCopyContractor(file,"d:\\",15);
fileCopyContractor.assignWork();
}
}
下面介绍一下线程的优先级:
如上所示, 它表示该线程被线程调度器选中的概率,其值越大,被线程调度器选中的概率越大,那么就会优先执行完毕。如下代码演示:
package com.Jevin.thread.demo3;
public class ThreadTest1 extends Thread {
public ThreadTest1(String name){
super(name);
}
/**
* 我们把要执行的操作写在run()方法里面,run()方法又被称为线程体;
*/
@Override
public void run() {
for(int i=0;i<1000000;i++){
System.out.println(this.getName()+"在计数:i="+i+",线程优先级="+this.getPriority());
}
}
}
package com.Jevin.thread.demo3;
public class ThreadTest2 extends Thread {
public ThreadTest2(String name){
super(name);
}
/**
* 我们把要执行的操作写在run()方法里面,run()方法又被称为线程体;
*/
@Override
public void run() {
for(int i=0;i<1000000;i++){
System.err.println(this.getName()+"在计数:i="+i+",线程优先级="+this.getPriority());
}
}
}
package com.Jevin.thread.demo3;
public class ThreadMain {
public static void main(String[] args) {
//创建线程对象:
ThreadTest1 t1 = new ThreadTest1("小黑线程");
ThreadTest2 t2 = new ThreadTest2("小红线程");
/**
* 设置线程的优先级
* 注意:改变线程的优先级需要在线程启动之前进行,否则会导致异常
*/
t1.setPriority(Thread.MAX_PRIORITY); //最高优先级
t2.setPriority(Thread.MIN_PRIORITY); //最低优先级
/**
* 线程启动正确的方式:
* 启动线程的时候,要调用线程的start()方法,线程启动之后会自动执行线程体(也就是run()方法)
*/
t1.start();
t2.start();
}
}
但是,我们还是别动其值比较好,默认的各占50%就好,有一个极端情况,加入垃圾回收线程被设置为最低的优先级,那么其被选中回收堆中无用的对象的概率大大降低,那么会造成堆内存越来越大,这样会极大的影响程序的运行,从而导致死机。
下面介绍两个相似的方法:yield()和sleep()方法的使用:
yield()方法翻译过来是:当前线程放弃当前该处理器的使用;
两种理解:(1)该线程yield()后放弃执行,让其他的线程执行
(2)该线程yield()后放弃执行,回到线程就绪队列,有可能被线程调度器选中,再次执行
下面由代码演示:
package com.Jevin.thread.demo3;
public class ThreadTest1 extends Thread {
public ThreadTest1(String name){
super(name);
}
/**
* 我们把要执行的操作写在run()方法里面,run()方法又被称为线程体;
*/
@Override
public void run() {
for(int i=0;i<1000000;i++){
System.out.println(this.getName()+"在计数:i="+i+",线程优先级="+this.getPriority());
/**
* 这里小黑线程每次打印一句,就yield(),即为放弃的意思:
* 1.放弃执行,让小红线程执行;
* 2.放弃执行,重新回到线程就绪队列,有可能被线程调度器选中,再次执行
* 如果是第一种情况,那么小黑线程应该是不连续打印的,但是实际控制台情况是小黑线程连续执行了,
* 那么他的功能是第二种情况。
* 这样造成的结果是:虽然小黑线程的优先级远远高于小红线程,但是由于yield()后,
* 即使被选中,也放弃本次的执行,小红线程的执行速度会迎头赶上小黑线程
*/
Thread.yield();
}
}
}
package com.Jevin.thread.demo3;
public class ThreadTest2 extends Thread {
public ThreadTest2(String name){
super(name);
}
/**
* 我们把要执行的操作写在run()方法里面,run()方法又被称为线程体;
*/
@Override
public void run() {
for(int i=0;i<1000000;i++){
System.err.println(this.getName()+"在计数:i="+i+",线程优先级="+this.getPriority());
}
}
}
package com.Jevin.thread.demo3;
public class ThreadMain {
public static void main(String[] args) {
//创建线程对象:
ThreadTest1 t1 = new ThreadTest1("小黑线程");
ThreadTest2 t2 = new ThreadTest2("小红线程");
/**
* 设置线程的优先级
* 注意:改变线程的优先级需要在线程启动之前进行,否则会导致异常
*/
t1.setPriority(Thread.MAX_PRIORITY); //最高优先级
t2.setPriority(Thread.MIN_PRIORITY); //最低优先级
/**
* 线程启动正确的方式:
* 启动线程的时候,要调用线程的start()方法,线程启动之后会自动执行线程体(也就是run()方法)
*/
t1.start();
t2.start();
}
}
上述我们在小黑线程体中yield(),如果是第一种情况,那么,小黑线程应该是不连续打印的,但是实际情形是连续的,那么yield()的正确理解是第二种情况。且我们的小黑线程的优先级远远高于小红线程,如果是第二种情况的话,那么就相当于小红线程被线程调度器选中的概率大大增加,会赶上显黑线程的执行情况,如下:
没有yield()的情况:可见小红线程被小黑线程远远甩在后面:
yield()后的情况:两个线程的运行相差无几,表明小红线程的优先级似乎提高了。
sleep(long millis)的翻译是:当前线程暂时停止millis秒钟,也就是说当前线程在暂停期间,不处于线程就绪队列,也就不会被线程调度器选中,那么线程调度器只能选择其他的线程去运行,当该线程sleep结束后,重新返回就绪队列,并有可能被线程调度器重新选中。
代码如下:
package com.Jevin.thread.demo3;
public class ThreadTest1 extends Thread {
public ThreadTest1(String name){
super(name);
}
/**
* 我们把要执行的操作写在run()方法里面,run()方法又被称为线程体;
*/
@Override
public void run() {
for(int i=0;i<1000000;i++){
System.out.println(this.getName()+"在计数:i="+i+",线程优先级="+this.getPriority());
}
}
}
package com.Jevin.thread.demo3;
public class ThreadTest2 extends Thread {
public ThreadTest2(String name){
super(name);
}
/**
* 我们把要执行的操作写在run()方法里面,run()方法又被称为线程体;
*/
@Override
public void run() {
for(int i=0;i<1000000;i++){
System.err.println(this.getName()+"在计数:i="+i+",线程优先级="+this.getPriority());
try {
//该线程sleep期间,是不会回到就绪队列的,当睡醒之后,才会重新回到就绪队列
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package com.Jevin.thread.demo3;
public class ThreadMain {
public static void main(String[] args) {
//创建线程对象:
ThreadTest1 t1 = new ThreadTest1("小黑线程");
ThreadTest2 t2 = new ThreadTest2("小红线程");
/**
* 线程启动正确的方式:
* 启动线程的时候,要调用线程的start()方法,线程启动之后会自动执行线程体(也就是run()方法)
*/
t1.start();
t2.start();
}
}
我们注意到:(1)sleep(long millis)和yield()方法都是static方法,也就是说和对象没关系;
(2)在哪个线程中调用sleep()或yield()方法,哪个线程就睡觉或暂停;
如下演示:
package com.Jevin.thread.demo3;
public class ThreadMain {
public static void main(String[] args) {
//创建线程对象:
ThreadTest1 t1 = new ThreadTest1("小黑线程");
ThreadTest2 t2 = new ThreadTest2("小红线程");
try {
/**
* 我们虽然调用的是t1.sleep(),但是和t1对象没有任何关系
* 我们是在主线程中调用的sleep(),所以这里是主线程睡觉
*/
t1.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
* 线程启动正确的方式:
* 启动线程的时候,要调用线程的start()方法,线程启动之后会自动执行线程体(也就是run()方法)
*/
t1.start();
t2.start();
}
}
如上所示,虽然我们使用的是t1.sleep(10000),但是实际上睡的不是t1线程,而是主线程,所以代码的实际运行情况是:点击run运行时,10秒钟内控制台没有任何反应,10秒钟后t1和t2线程相继运行。
下面介绍一下join()方法的运用:
jdk中的解释很简单,即为:等待该线程终结; 换言之,只要该线程没有终结,其他线程不会执行。
下面我们用代码演示:达到一个这个需求:在主线程中创建一个包含50个元素的数组,然后创建一个初始化数组的线程对象来初始化数组,只有当初始化数组线程将数组元素初始化完成之后,在主线程中打印数组元素,如果数组元素没有初始化完成,主线程中会打印一堆的null,没有意义;
package com.Jevin.thread.demo7;
/**
* 初始化数组线程
*/
public class InitArrayThread extends Thread {
private String[] arr;
public InitArrayThread(String name, String[] arr) {
super(name);
this.arr = arr;
start();
}
@Override
public void run() {
for (int i = 0; i < arr.length; i++) {
String str = "hello-" + i;
arr[i] = str;
System.err.println(this.getName() + "正在初始化元素:" + str);
}
}
}
package com.Jevin.thread.demo7;
public class MainThread {
public static void main(String[] args) {
/**
* 在主线程中创建一个包含50个元素的数组,然后创建一个初始化数组的线程对象来初始化数组
* 只有当初始化数组线程将数组元素初始化完成之后,在主线程中打印数组元素
* 如果数组元素没有初始化完成,主线程中会打印一堆的null,没有意义
*/
String[] arr = new String[50];
//创建初始化数组线程
InitArrayThread t1 = new InitArrayThread("初始化线程", arr);
//在主线程中打印数组元素
for (int i = 0; i < arr.length; i++) {
System.out.println("在主线程中打印数组元素,arr[" + i + "]=" + arr[i]);
}
}
}
我们需要的是当初始化线程将数组初始化完成之后,然后主线程中打印这些数组元素,换言之:主线程和初始化线程都应该有元素的,不为空;但实际情况是,主线程中为空;那么,我们如何达到我们的要求呢?这就要用到join()方法。一下代码演示:
package com.Jevin.thread.demo7;
public class MainThread {
public static void main(String[] args) {
/**
* 在主线程中创建一个包含50个元素的数组,然后创建一个初始化数组的线程对象来初始化数组
* 只有当初始化数组线程将数组元素初始化完成之后,在主线程中打印数组元素
* 如果数组元素没有初始化完成,主线程中会打印一堆的null,没有意义
*/
String[] arr = new String[500];
//创建初始化数组线程
InitArrayThread t1 = new InitArrayThread("初始化线程", arr);
//不准确的控制方式
//try {
// /**
// * 主线程睡10毫秒,在这10毫秒之内,线程就绪队列中只有初始化数组线程;
// * 换言之:在主线程睡的10毫秒内,初始化线程将数组初始化完成
// * 但是,我们不知道主线程到底要睡多少时间,初始化线程才能初始化数组完成,随意这个时间是蒙出来的,不准确
// */
// Thread.sleep(10);
//} catch (InterruptedException e) {
// e.printStackTrace();
//}
//准确的控制方式
try {
/**
* 我们在主线程中调用t1线程的join()方法,则主线程会被阻塞,
* 直到t1线程运行结束,主线程才会解除阻塞,重新回到就绪队列
* 换言之:只要t1线程没有运行结束,主线程就不会执行
*/
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
//在主线程中打印数组元素
for (int i = 0; i < arr.length; i++) {
System.out.println("在主线程中打印数组元素,arr[" + i + "]=" + arr[i]);
}
}
}
下面介绍一个银行取款使用线程的知识,如下模型:
我们先分析一下:按照线程的执行原理:在自己的时间片段内交替执行。如果小明线程执行到第2步,1000>600,走第3步,但是这个时候,小明线程的时间片段到了,但遗憾的是,小明还没有取到钱;再走小红片段,也走到第2步,1000>900,走第3步,此时,小红的时间片也到了;再走小明线程,小明取走了600元,再走小红,小红取走了900元;那么,600+900>1000元,银行亏了?
下面代码演示:
package com.Jevin.thread.demo8;
/**
* 账户类
*/
public class Account {
private int balance = 1000; //账户余额
/**
* 取款方法
*
* @param money 取款金额
* @return
*/
public int withdrawal(int money) throws Exception {
if (this.balance >= money) {
/**
* 这里Thread.sleep(10);并不是让账户对象睡觉,因为账户对象不是线程
* 这里的取款方法是线程对象调用的,所以哪个线程调用该方法,那个线程睡觉
*/
Thread.sleep(10); //用来模拟当前线程时间片段结束
this.balance -= money;
return money;
} else {
throw new Exception("账户余额不足");
}
}
}
package com.Jevin.thread.demo8;
/**
* 小明线程
*/
public class XiaomingThread extends Thread {
private Account account;
public XiaomingThread(String name,Account account){
super(name);
this.account=account;
start();
}
@Override
public void run() {
try {
int money = account.withdrawal(600);
System.out.println(this.getName()+"取款成功,金额是:"+money);
} catch (Exception e) {
System.err.println(this.getName()+"取款遇到异常,异常信息是:"+e.getMessage());
e.printStackTrace();
}
}
}
package com.Jevin.thread.demo8;
/**
* 小红线程
*/
public class XiaohongThread extends Thread {
private Account account;
public XiaohongThread(String name,Account account){
super(name);
this.account=account;
start();
}
@Override
public void run() {
try {
int money = account.withdrawal(900);
System.out.println(this.getName()+"取款成功,金额是:"+money);
} catch (Exception e) {
System.err.println(this.getName()+"取款遇到异常,异常信息是:"+e.getMessage());
e.printStackTrace();
}
}
}
package com.Jevin.thread.demo8;
public class ThreadMain {
public static void main(String[] args) {
//创建账户对象:
Account account = new Account();
//创建线程对象:
XiaomingThread t1 = new XiaomingThread("小明",account);
XiaohongThread t2 = new XiaohongThread("小红",account);
}
}
执行结果为:符合预期结果
那么该如何解决这个问题呢?我们可以使用synchronized(this){}同步块来解决,如下:
/**
* 取款方法
*
* @param money 取款金额
* @return
*/
public int withdrawal(int money) throws Exception {
/**
*synchronized (this){}称为"同步块",
* 当一个线程A获得锁进入同步块之后,其他线程无法进入到这个同步块的,
* 只有线程A执行完毕,从同步块中退出之后,其他线程才能竞争获得锁进入同步块
*/
synchronized (this){
System.out.println(Thread.currentThread().getName()+"进入到同步块");
if (this.balance >= money) {
/**
* 这里Thread.sleep(10);并不是让账户对象睡觉,因为账户对象不是线程
* 这里的取款方法是线程对象调用的,所以哪个线程调用该方法,那个线程睡觉、
*
* 当一个线程获得锁进入同步块之后,即使执行了sleep(long millis)/yield()也是不会放锁的
* 如果执行的是sleep(long millis),线程不会放锁的,而是睡醒之后继续执行
* 如果执行的是yield(),则该线程不会放弃CPU,而是继续执行(如果没有锁,则该线程放弃CPU,重新回到就绪队列)
*/
Thread.sleep(10000); //用来模拟当前线程时间片段结束
this.balance -= money;
return money;
} else {
throw new Exception("账户余额不足");
}
}
}
运行结果如下:
完美解决。
上述synchronized(this){} 1.锁的是方法的全部代码;2.并且是使用this作为锁的;那么可以将synchronized提到该方法的返回值之前,如下:
/**
* 取款方法
*
* @param money 取款金额
* @return
*/
public synchronized int withdrawal(int money) throws Exception {
System.out.println(Thread.currentThread().getName()+"进入到同步块");
if (this.balance >= money) {
Thread.sleep(10000); //用来模拟当前线程时间片段结束
this.balance -= money;
return money;
} else {
throw new Exception("账户余额不足");
}
}
那么何时需要线程同步呢?即为:当多个线程同时修改相同数据的时候,必须要线程同步,给数据加锁;
但是线程同步不能滥用,因为线程同步是以牺牲执行速度为代价的!
一下一些线程同步的例子,可以看看:
================================================================================================
下面简单的使用多线程实现一个买票功能:
(1)创建一个车票类Ticket,用String数组保存数据,共有100张车票,也就是该数据保存100个数据
提供初始化车票分方法public void initTicket(String ticNo){}
提供卖票的方法,public String sellTicket(){}
(2)创建一个初始化车票线程来初始化车票对象,也就是该线程循环100次,初始化车票
(3)创建4个卖票线程对象,每个卖票线程卖30次(票不够卖)
(4)只有初始化车票的线程把车票初始化完成之后,才能开始卖票
package com.Jevin.thread.demo9;
/**
* (1)创建一个车票类Ticket,用String数组保存数据,共有100张车票,也就是该数据保存100个数据 提供初始化车票分方法public void initTicket(String ticNo){} 提供卖票的方法,public String
* sellTicket(){}
*/
public class Ticket {
/**
* 保存车票的数组
*/
private String[] arr = new String[100];
private int index = -1;
/**
* 初始化车票的方法,也就是将数据保存到数组中,每次初始化一张车票
*
* @param ticketNo
*/
public void initTicket(String ticketNo) throws Exception {
if (index < arr.length - 1) {
index++;
arr[index]=ticketNo;
}else{
throw new Exception("车票满了");
}
}
/**
* 卖票的方法,返回一张卖出的车票,每次卖票一张
*
* @return
*/
public synchronized String sellTicket() throws Exception {
if(index>=0){
String ticketNo=arr[index];
/**
* 这里会出现“重票”的问题
*/
//Thread.yield(); //重票概率小
//Thread.sleep(10); //重票概率大
arr[index]=null;
index--;
return ticketNo;
}else{
throw new Exception("车票卖完了");
}
}
}
package com.Jevin.thread.demo9;
/**
*创建一个初始化车票线程来初始化车票对象,也就是该线程循环100次,初始化车票
*/
public class InitTicketThread extends Thread {
private Ticket ticket;
public InitTicketThread(String name,Ticket ticket){
super(name);
this.ticket=ticket;
start();
}
@Override
public void run() {
try {
for(int i=0;i<200;i++){
String ticketNo="第"+i+"号车票";
ticket.initTicket(ticketNo);
System.err.println(this.getName()+"初始化车票成功,车票是:"+ticketNo);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
package com.Jevin.thread.demo9;
/**
* 卖票线程,每个卖票线程卖30次
*/
public class SellTicketThread extends Thread {
private Ticket ticket;
public SellTicketThread(String name, Ticket ticket) {
super(name);
this.ticket = ticket;
start();
}
@Override
public void run() {
try {
for (int i = 1; i <= 30; i++) {
String ticketNo = ticket.sellTicket();
System.out.println(this.getName() + "第" + i + "次卖票成功,卖的车票是" + ticketNo);
}
} catch (Exception e) {
System.err.println(this.getName() + "卖票遇到异常,异常信息是:" + e.getMessage());
e.printStackTrace();
}
}
}
package com.Jevin.thread.demo9;
public class TicketMain {
public static void main(String[] args) {
//创建车票对象:
Ticket ticket = new Ticket();
//创建初始化车票线程对象:
InitTicketThread initTicketThread = new InitTicketThread("初始化线程对象",ticket);
//只有初始化车票的线程把车票初始化完成之后,才能开始卖票
try {
initTicketThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
//创建四个卖票线程:
SellTicketThread s1=new SellTicketThread("卖票线程1",ticket);
SellTicketThread s2=new SellTicketThread("卖票线程2",ticket);
SellTicketThread s3=new SellTicketThread("卖票线程3",ticket);
SellTicketThread s4=new SellTicketThread("卖票线程4",ticket);
}
}
那么,我们考虑另一个问题?多个线程同时访问同一个对象上不同的方法,修改相同的数据,这种情况有可能发生吗?答案是肯定会。这才是正常的情况,例如:订票和退票,存款和取款等。
我们创建一个后进先出的Stack栈类,一个线程专门压栈,一个线程专门弹栈;
package com.Jevin.thread.demo10;
/**
* 遇到的问题:
* 1.消费者取出null
* 2.消费者遇到-1的数组索引越界异常
* 3.生产者者遇到100的数组索引越界异常
*
* 解决以上的问题:
* (1)给push()和pop()加synchronized,可以解决消费者取出null的问题
* (2)利用wait()和notify()解决消费者和生产者数组越界异常
*/
public class Stack {
private String[] arr = new String[100];
private int index = -1;
private boolean dataIsReady = false; //数据是否准备好,false表示没数据
/**
* 压栈
*
* @param str
*/
//当生产者访问压栈的方法时,消费者无法访问弹栈的方法:
public synchronized void push(String str) {
//当数组中没有数据的时候(dataIsReady = false),生产者应该生产数据,改变标志变量,唤醒消费者线程
//当数组中有数据的时候(dataIsReady = true),生产者应该被阻塞
try {
if(dataIsReady){
this.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
index++;
Thread.yield();
arr[index] = str;
dataIsReady = true; //此时,数组中已经放入数据,改变标志变量
this.notifyAll(); //唤醒消费者线程
}
/**
* 弹栈
*
* @return
*/
//当消费者访问弹栈的方法时,生产者无法访问压栈的方法:
public synchronized String pop() {
//当数组中没有数据的时候(dataIsReady = false),消费者应该被阻塞
//当数组中有数据的时候(dataIsReady = true),消费者应该消费数据,改变标志变量,唤醒生产者线程
try {
if(!dataIsReady){
this.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
String str = arr[index];
Thread.yield();
arr[index] = null;
index--;
dataIsReady = false; //此时,数组中没有数据,改变标志变量
this.notifyAll(); //唤醒生产者
return str;
}
}
package com.Jevin.thread.demo10;
/**
* 生产者线程,专门用来压栈
*/
public class ProducerThread extends Thread {
private Stack stack;
public ProducerThread(String name, Stack stack) {
super(name);
this.stack = stack;
start();
}
@Override
public void run() {
for (int i = 0; i < 200; i++) {
String str = "Hello-" + i;
stack.push(str);
System.err.println(this.getName() + "第" + i + "次压栈成功,压栈的数据是:" + str);
Thread.yield();
}
}
}
package com.Jevin.thread.demo10;
/**
* 消费者线程,专门用来弹栈
*/
public class ConsumerThread extends Thread {
private Stack stack;
public ConsumerThread(String name, Stack stack) {
super(name);
this.stack = stack;
start();
}
@Override
public void run() {
for (int i = 1; i < 200; i++) {
String str = stack.pop();
System.out.println(this.getName() + "第" + i + "次弹栈成功,数据是:" + str);
Thread.yield();
}
}
}
package com.Jevin.thread.demo10;
public class ThreadMain {
public static void main(String[] args) {
//创建栈对象:
Stack stack = new Stack();
//创建生产者线程:
ProducerThread producerThread = new ProducerThread("生产者线程", stack);
//创建消费者线程:
ConsumerThread consumerThread = new ConsumerThread("消费者线程", stack);
}
}
下面介绍另一个有意思的问题:如何使用同步的集合?例如:一个线程向集合中添加数据,另一个线程从集合中删除数据:
有以下几种思路:
(1)使用一些线程安全的老的集合:如Vector,HashMap等
(2)可以使用Collections中的方法,将线程不安全的集合转变为线程安全的集合,如下:
线程这块大致到这里,以后有机会再补充!!!