Java基础--线程和多线程的应用及问题处理

概述::
几乎所有的操作系统都支持同时运行多个任务,一个任务通常就是一个程序,每个运行中的程序就是一个进程。当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程。

线程和进程:

  1. 进程:是一个正在执行的程序,每一个进程执行都有一定的执行顺序,该顺序是一个执行路径,或者叫一个控制单元.
  2. 线程:是进程中一个独立的控制单元.线程在控制着进程的执行进程与线程的关系:一个进程中至少有一个线程.

进程至少一个线程负责java程序的执行,而且这个线程的运行代码存在于main方法中,该线程成为主线程.
扩展:
其实更细节的说明jvm,jvm不只一个线程,还有负责垃圾回收机制的线程.
第一讲>>>线程的创建与使用

线程的实现方式之一
通过对api的查找,java已经提供了对线程这类事物的描述,就Thread类.创建线程的第一种方式: 继承Thread类.
步骤:

1. 定义类继承Thread
2. 复写Thread类中的run方法

线程都有自己默认的名称,通过getName()就可以获取.Thread-0123456….
static Thread currentThread():获取当前线程对象
getName():获取线程名称
设置线程名称:setName或者构造函数.

class 类名 extends Thread{
方法1;
方法2;
…
public void run(){
// other code…
}
属性1;
属性2;
…
 
}

//下面我们举个例子说明下这个方法:
/**
 继承Thread类,直接调用run方法
 * */
class hello extends Thread {
  
    private String name;
    public hello() {
 
    }
 
    public hello(String name) {
        this.name = name;
    }
 
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(name + "运行     " + i);
        }
    }
 
    public static void main(String[] args) {
        hello h1=new hello("A");
        hello h2=new hello("B");
        h1.run();
        h2.run();
    }

}

当我们直接调用run()方法,结果可想而知,跟普通方法没什么区别,是顺序且有序的.

public static void main(String[] args) {
        hello h1=new hello("A");
        hello h2=new hello("B");
        h1.start();
        h2.start();
    }

当我们换个方式去调用start()方法,就可以发现有新的现象,变得
无序且每次运行结果都不一样.注意:虽然我们在这里调用的是start()方法,但是实际上调用的还是run()方法的主体。

那么:为什么我们不能直接调用run()方法呢?

我的理解是:线程的运行需要本地操作系统的支持。

如果你查看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 || this != me)
            throw new IllegalThreadStateException();
        group.add(this);
        start0();
        if (stopBeforeStart) {
        stop0(throwableFromStop);
    }
}
private native void start0();

这个这个方法用了native关键字,次关键字表示调用本地操作系统的函数。因为多线程的实现需要本地操作系统的支持。

目的:将自定义的代码存储在run方法中让线程运行.

 调用该线程的start方法.该方法有两种作用:
 
		1.启动线程
		2.调用run方法.

但是start方法重复调用的话,会出现java.lang.IllegalThreadStateException异常。
每个线程都有cpu的执行权,cpu执行谁,谁就运行.明确一点:在某一时刻,只能有一个程序在运行,(多核除外).cpu在做着快速的切换,以达到看上去像是同时运行的效果.
我们可以形象的把多线程的运行行为看作互相抢夺cpu执行权,随机性,谁抢到谁执行,至于执行多久cpu说了算.
这里特地提醒一下大家:main方法其实也是一个线程。在java中所以的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先得到CPU的资源。
在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个jVM实习在就是在操作系统中启动了一个进程。

为什么要覆盖run方法呢?
Thread类用于描述线程,该类定义了一个功能用于存储线程要运行的代码,该存储功能就是run方法里面的代码.

Start调用的就是run方法,demo.start()开启线程并执行线程的run方法
直接调用对象的run’方法的时候,仅仅是对象的调用方法,而线程创建了并没有运行.

线程的实现方式之二

下面我们看实现多线程的第二种方式:
创建线程的第二种方式:实现runnable接口
步骤:

1. 实现runnable接口
2. 覆盖runnable接口中的run方法

将线程要运行的代码存放在run方法中

1. 通过thread类建立线程对象
2. 将runnable接口的子类对象作为实际参数传递给thread类的构造函数

通过实现Runnable接口:

大致框架是:

class 类名 implements Runnable{
方法1;
方法2;
…
public void run(){
// other code…
}
属性1;
属性2;
…
 
}

/**
 * 实现Runnable接口
 * */
class hello implements Runnable {
  
    private String name;
    public hello() {
 
    }
 
    public hello(String name) {
        this.name = name;
    }
 
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(name + "运行     " + i);
        }
    }
 
    public static void main(String[] args) {
        hello h1=new hello("线程A");
        Thread demo= new Thread(h1);
        hello h2=new hello("线程B");
        Thread demo1=new Thread(h2);
        demo.start();
        demo1.start();
    }

}

关于选择继承Thread还是实现Runnable接口?

翻阅JDK文档,你会发现其实Thread也是实现Runnable接口.

为什么要将runnable接口中的子类对象传递给thread的构造函数,因为自定义的run方法所属的对象是runnable接口的子类对象,所以要让线程去指定对象的run方法.就必须明确该run方法所属的对象.调用thread类的start方法开启线程并调用runnable接口子类的run方法

实现方式和继承方式有什么区别呢?
实现方式:避免了单继承的局限性,在定义线程时建议使用该方式.
继承方式:线程代码存放在thread子类run方法中,实现时,线程代码存在接口的子类的run方法中.总结一下吧:

实现Runnable接口比继承Thread类所具有的优势:

  1. 适合多个相同的程序代码的线程去处理同一个资源
  2. 可以避免java中的单继承的限制
  3. 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立。

第二讲>>>线程的休眠与中断

public static void sleep(long millis,
int nanos)
throws InterruptedException
在指定的毫秒数加指定的纳秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。该线程不丢失任何监视器的所属权。

class Hello implements Runnable {
    public void run() {
        for (int i = 0; i < 3; i++) {
            try {
                Thread.sleep(2000);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + i);
        }
    }
 
    public static void main(String[] args) {
        Hello hello = new Hello();
        Thread demo = new Thread(hello, "线程");
        demo.start();
    }
}

【运行结果】:(每2秒打印一次)

线程0

线程1

线程2

停止线程:
Stop方法已经过时,如何停止线程?
只有一种,run方法结束.开启多线程运行代码通常都是循环结构,只要控制住循环就可以让线程也就是run方法结束.
定义一个标记值flag,while(flag)执行线程,定义一个改变flag的函数就可以令其停止.
特殊情况:当线程处于冻结状态,就不会读取到标记,那么线程就不会结束.
当没有指定的方式让冻结的线程回复到运行状态时,就需要对冻结进行清除,强制让线程回复到运行状态来,这样就可以操作标记让线程结束.
Thread类中提供了该方法,叫做interrupt方法.
如果当前线程没有中断它自己(这在任何情况下都是允许的),则该线程的 checkAccess 方法就会被调用,这可能抛出 SecurityException。

如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,或者该类的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法过程中受阻,则其中断状态将被清除,它还将收到一个 InterruptedException。

如果该线程在可中断的通道上的 I/O 操作中受阻,则该通道将被关闭,该线程的中断状态将被设置并且该线程将收到一个 ClosedByInterruptException。

如果该线程在一个 Selector 中受阻,则该线程的中断状态将被设置,它将立即从选择操作返回,并可能带有一个非零值,就好像调用了选择器的 wakeup 方法一样。

如果以前的条件都没有保存,则该线程的中断状态将被设置。

中断一个不处于活动状态的线程不需要任何作用。

class hello implements Runnable {
    public void run() {
        System.out.println("执行run方法");
        try {
            Thread.sleep(10000);
            System.out.println("线程完成休眠");
        } catch (Exception e) {
            System.out.println("休眠未成功");
            return;  //返回到程序的调用处
        }
        System.out.println("正常运行完毕");
    }
 
    public static void main(String[] args) {
        hello hello = new hello();
        Thread demo = new Thread(hello, "线程");
        demo.start();
        try{
            Thread.sleep(2000);
        }catch (Exception e) {
            e.printStackTrace();
        }
        demo.interrupt(); //2秒后中断线程
    }
}

【运行结果】:

执行run方法

休眠被打断

守护线程:
SetDaemon() 将一个线程标记为守护/用户线程,当正在运行的线程都是守护线程的时候,jvm自动退出.

Join方法:谁调用这个方法,等谁死掉,主线程才开始工作.剥夺主线程的执行权.
当A线程执行到了B线程Join方法时,A就会等待,等B线程都执行完,A才会执行.join方法用来临时加入线程执行.

优先级:
代表抢资源的频率.
Yield方法:暂停当前正在执行的线程对象,并执行其他线程.
上面几种方法就不再一一举例.
当多条语句在操作同一线程共享的数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来,导致共享数据的错误.那么接下来我们来解决这个问题.

第三讲>>>多线程带来的安全问题和解决

说起来多线程,我们就容易想要同步,那么同步有什么利弊呢?
对多条操作共享数据的语句,只能让一个线程执行完,在执行过程中其他线程不可以参与执行.
Java对于多线程的安全问题提供了专业的解决方式,就是同步代码块.
Synchronized(对象){需要被同步的代码}
对象如同锁,只有持有锁的线程可以在同步中执行,没有的线程即使获取cpu的执行权也进不去.
同步的前提:

1. 必须要有两个或者两个以上的进程.
2. 必须是多个线程是用同一个锁.
3. 必须保证同步中只有一个线程在运行.

好处:解决了多线程的安全问题
弊端:如果多个线程,每个线程只要获取到cpu执行权,除了代码执行以外,都要判断这个锁的状态,消耗了大量资源.

同步关键字synchronized有两种使用形式:
1.同步代码块:

class Ticket12 implements Runnable {
	private int tick = 1000;
	Object obj = new Object();
	boolean flag = true;

	public void run()
	{
		if(flag)
		{
			while(true)
			{
				synchronized(obj)
				{
					if(tick>0)
					{
						try{Thread.sleep(10);}
						catch(Exception e){}
						System.out.println(Thread.currentThread().getName()+"::Codes::"+tick--);
					}
					else System.exit(0);
				}
			}
		}else {
			while(true)
				show();
		}
	}

	public synchronized void show() {
		if (tick > 0) {
			try {Thread.sleep(5);}catch (InterruptedException e) {}
			System.out.println(Thread.currentThread().getName() + "::Function::" + tick--);
		}
		else System.exit(0);
	}
}

2.同步函数:

class Ticket implements Runnable {
	private int tick = 100;
	Object obj = new Object();

	public synchronized void run() {

		while (true)
				if (tick > 0) {
					try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
						// TODO: handle exception
					}
					System.out.println(Thread.currentThread().getName() + "::Ticket::" + tick--);
				} else
					System.exit(0);
	}
}

这两种使用方式有几个不同之处:
1.
使用地点不同,效率不同.如果同步在函数上使用,效率比代码块稍低,因为整个函数中的代码同时只能有一个线程来执行
2."锁"的不同:
同步代码块中使用同步,我们可以自定义一个对象来作为同步锁,java虚拟机会自动给这个对象加上一个标记flag作为锁的标记来判断是否有线程正在使用锁,正在执行锁内的代码.
同步函数使用的锁是this,就是调用函数的对象.当这个函数是静态的时候,由于没有对象,锁为实现多线程的类Class.class,注意这是一个二进制文件,使用文件作为锁对象.

接下来我们谈一下单例设计模式中的同步问题:

class Single   //饿汉式  建议使用
{
	private Single(){}
	private static Single s = new Single();
	public static Single getIns()
	{
		return s;
	}
}

在饿汉式单例设计模式中,涉及到获取对象的代码只有一句,而且要获取的这个对象只有一个,因此有效的保证了对象在内存中的唯一.

class Single2  //懒汉式
{
	private Single2(){}
	private static Single2 s = null;
	public static Single2 getIns()
	{
		if (s==null)
			s = new Single2();
		return s;
	}
}

懒汉式则是由三句代码来获取对象,如果不加以控制,那么当线程并发对这个获取对象的方法执行的时候,根本难以保障对象的唯一性.在任意一步停下来,其他线程抢到了执行权,那么自己醒来以后,线程很可能就不是唯一的了.
因此我们要使用同步来改进这个懒汉式.

class Single3{
	private static Single s = null;
	private Single(){}
	Object obj = new Object();
	public static Single getIns()
	{
		synchronized(Single.class)
		{
			if(s == null)
				s = new Single();
		}
		return s;
	
	}
}

即使加了同步,多个线程并发访问,在执行带判断锁那里执行权被其他线程抢到了,这时候,其他线程进入,执行到if(s == null),然后执行权被抢走,这时候自己醒来以后,已经判断过锁是可用的,那么直接进入锁内代码,新建一个对象,当另外一个线程获取到执行权以后并不知道对象已经存在了,因此又会再次新建一个对象.这时候这个单例设计模式已经不是我们所需要的单例设计模式了.
那么我们应该如何解决这个问题呢?要杜绝上面的情况发生,我们可以在线程判断同步代码锁之前再判断一次对象是否存在,代码如下.
再次改进代码:

class Single{
	private static Single s = null;
	private Single(){}
	Object obj = new Object();
	public static Single getIns()
	{
		if(s == null)
		{
			synchronized(Single.class)
			{
				if(s == null)
					s = new Single();
			}
	}
		return s;
	
	}
}

同步带来的问题:死锁

引用大学操作系统课本中的一句话,
虽然进程在运行过程中,可能发生死锁,但死锁的发生也必须具备一定的条件,死锁的发生必须具备以下四个必要条件。

1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。

2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。

3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。

4)环路等待条件:指在发生死锁时,必然存在一个进程–资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

当两个线程同时使用两个相同的锁嵌套同步,锁顺序相反,

package test3;

public class DeadLockDemo {

	public static void main(String[] args) {
		// TODO 自动生成的方法存根
		Thread t1 = new Thread(new Test(true));
		Thread t2 = new Thread(new Test(false));
		t1.start();
		t2.start();
	}

}

class MyLock {
	static Object a = new Object();
	static Object b = new Object();
}

class Test implements Runnable {

	boolean flag;

	Test(boolean flag) {
		this.flag = flag;
	}

	public void run() {
		if (flag) {
			while (true) {
				synchronized (MyLock.a) {
					System.out.println("if locka");
					synchronized (MyLock.b) {
						System.out.println("if lockb");
					}
				}
			}
		} else {
			while (true) {
				synchronized (MyLock.b) {
					System.out.println("else b");
					synchronized (MyLock.a) {
						System.out.println("else a");
					}
				}
			}
		}
	}
}

第四讲>>>线程之间的通讯
等待唤醒机制:
加一个操作标志,操作过后更改该标记的值,每次进入同步代码后判断一次该标记的值.如果符合条件则进入操作数据的代码段.如果不符合,则唤醒其他线程之后,挂起自己的线程
Wait()
Notify()
两个方法实现上述.

Wait()
Notify()
Notifyall()
这三个方法都是用在同步中,因为要对持有监视器(锁)的线程操作.
所以要定义使用在同步中,因为只有同步才具有锁的概念.
为什么这些操作线程的方法要定义在object类中呢?
因为这些方法在操作同步中线程时都必要要表示它们所操作线程持有的锁,只有同一个锁上的被等待线程可以被同一个锁上的notify唤醒,不可以对不同锁中的线程进行notify()方法.
也就是说等待和唤醒必须是同一把锁,而锁可以是任意对象,所以可以被任意对象调用的方法应该定义在object类中.

我们可以举个生产者消费者的例子说明一下这个问题:
每当生产者生产一个产品,消费者则来消费,当没有产品的时候消费者不能消费,只能生产者来生产.当有产品存在的时候,不能再生产,只能消费者来消费.通过这样的前提我们可以写出这样的代码来:

package newlyg;

public class ProducerConsumerDemo {

	public static void main(String[] args) {
		// TODO 自动生成的方法存根
		Res r = new Res();
		Producer pro = new Producer(r);
		Consumer con = new Consumer(r);
		//要避免问题也可以定义多个对象,定义count为static
		Thread t1 = new Thread(con);
		Thread t3 = new Thread(con);
		Thread t2 = new Thread(pro);
		Thread t4 = new Thread(pro);
		
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}

}
class Res
{
	private String name;
	private int count = 1 ;
	private boolean flag = false;
	
	public synchronized void set(String name)
	{
		while(flag)//if改成while 就可以避免线程进入if判断后直接跳入下面的代码不再判断标记
			try{this.wait();}catch(Exception e){}
		this.name = name+"--"+count++;
		System.out.println(Thread.currentThread().getName()+"...Producer........"+this.name);
		flag = true;
		this.notifyAll();
	}
	public synchronized void  out()
	{
		while(!flag)
			try{this.wait();}catch(Exception e){}
			
		System.out.println(Thread.currentThread().getName()+"...Consumer..."+this.name);
		flag = false;
		this.notifyAll();
	}
}
class Producer implements Runnable
{
	private Res r;
	Producer(Res r)
	{
		this.r = r;
	}
	public void run()
	{
		while(true)
			r.set("+Pruduct");
	}
}
class Consumer implements Runnable
{
	private Res r ;
	 Consumer(Res r)
	 {
		 this.r = r;
	 }
	 public void run()
	 {
		 while(true)
			 r.out();
	 }
}

JDK1.5以后后,synchronized被Condition,lock(),unlock()替代.将同步synchronized替换成现实lock操作,将object中的wait(),notify()/notifyAll()替换成了condition对象,该对象可以由lock锁进行获取.可以实现己方唤醒对方的操作.public interface ConditionCondition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。

条件(也称为条件队列 或条件变量)为线程提供了一个含义,以便在某个状态条件现在可能为 true 的另一个线程通知它之前,一直挂起该线程(即让其“等待”)。因为访问此共享状态信息发生在不同的线程中,所以它必须受保护,因此要将某种形式的锁与该条件相关联。等待提供一个条件的主要属性是:以原子方式 释放相关的锁,并挂起当前线程,就像 Object.wait 做的那样。

Condition 实例实质上被绑定到一个锁上。要为特定 Lock 实例获得 Condition 实例,请使用其 newCondition() 方法。

作为一个示例,假定有一个绑定的缓冲区,它支持 put 和 take 方法。如果试图在空的缓冲区上执行 take 操作,则在某一个项变得可用之前,线程将一直阻塞;如果试图在满的缓冲区上执行 put 操作,则在有空间变得可用之前,线程将一直阻塞。我们喜欢在单独的等待 set 中保存 put 线程和 take 线程,这样就可以在缓冲区中的项或空间变得可用时利用最佳规划,一次只通知一个线程。可以使用两个 Condition 实例来做到这一点。

Lock里面可以有多个condition对象,所以要避免唤醒同类线程的话,可以把己方和对方定义为两种condition对象,完成操作后唤醒对方的线程.(互相唤醒)
我们可以使用lock() unlock()改进以上代码

package newlyg;

import java.util.concurrent.locks.*;

public class LockUnlockDemo {

	public static void main(String[] args) {
		// TODO 自动生成的方法存根
		Resource r = new Resource();
		
		new Thread(new Producer(r)).start();
		new Thread(new Consumer(r)).start();
	}

}

class Resource {
	private int count = 0;
	private boolean flag = false;
	private ReentrantLock lock = new ReentrantLock();
	private Condition con = lock.newCondition();
	private Condition pro = lock.newCondition();

	public void set() throws InterruptedException {
		lock.lock();
		try {
			while (flag)
				con.await();
			System.out.println(Thread.currentThread().getName() + "...Producer..." + count++);
			flag = true;
			pro.signalAll();
		} finally {
			// TODO: handle finally clause
			lock.unlock();
		}
	}

	public void out() throws InterruptedException {
		lock.lock();
		try {
			while (!flag)
				pro.await();
			System.out.println(Thread.currentThread().getName() + "...Consumer..." + count);
			flag = false;
			con.signalAll();

		} finally {
			lock.unlock();
		}
	}
}

class Producer implements Runnable {
	private Resource r;

	Producer(Resource r) {
		this.r = r;
	}

	public void run() {
		while (true) {
			try {
				r.set();
			} catch (Exception e) {
				// TODO: handle exception
			}
		}
	}
}

class Consumer implements Runnable {
	private Resource r;

	Consumer(Resource r)
	{
		this.r = r;
	}

	public void run() {
		while (true)
			try {
				r.out();
			} catch (Exception e) {
				// TODO: handle exception
			}
	}
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值