java多线程(1)基础

时间:2021-06-05 17:29:07

一、创建线程的两种方法

①继承Thread类

class MyThread extends Thread{
@Override
public void run(){
}
}

MyThread mt = new MyThread();
mt.start();

②实现Runnable接口

class MyThread implements Runnable{
@Override
public void run(){
}
}

MyThread mt = new MyThread();
Thread td = new Thread(mt);
td.start();

二、两种方式的比较(火车站买票)

① Runnable方式可以避免Thread方式由于Java单继承特性带来的缺陷。
②Runnable的代码可以被多个线程共享,适合于多个线程处理同一个资源的情况。

class Ticket implements Runnable{
private int ticket = 100;
@Override
public void run(){
while(true){
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"sale"+tick--);
}
}
}
}

class TicketDemo{
public static void main(String[] args){
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
t1.start();
t2.start();
t3.start();
}
}

存在的线程安全问题:会出现负票
分析:
ticket是t1、t2、t3共享的变量。当多个线程在操作共享数据时,一个线程对多条语句只执行了一部分,还没执行完,另一个线程参与进来执行,导致共享数据的错误。
解决方法:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。

Java对于多线程的安全问题提供了专业的解决方法:同步代码块

synchronized(对象){
需要同步的代码
}
class Ticket implements Runnable{
private int ticket = 100;
@Override
public void run(){
while(true){
synchronized(this){
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"sale"+tick--);
}
}
}
}
}

注:同步函数public synchronized void add()同步函数用的锁是this

三、懒汉式

class Single {
private static Single s = null;
private Single(){}

public static Single getInstance(){
if(s==null){
s = new Single();
}
return s;
}
}

经过上面的学习,发现这样写懒汉式,如果多线程访问时,就会出现安全问题,两个线程同时到达s=new Single()

如果使用同步函数:

class Single {
private static Single s = null;
private Single(){}

public static synchorized Single getInstance(){
if(s==null){
s = new Single();
}
return s;
}
}

但是这样的话,会影响执行的效率,改为如下:

class Single {
private static Single s = null;
private Single(){}

public static Single getInstance(){
if(s==null){
synchorized(Single.class){
if(s==null){
s = new Single();
}
}
}
return s;
}
}

四、线程同步

其实就是多个线程在操作同一个资源,但是操作的动作不同。

class Res{
String name;
String sex;
}

class Input implements Runnable{
private Res r;
Input(Res r)
{
this.r = r;
}
public void run(){
int x = 0;
while(true){
if(x==0){
r.name = "mike";
r.sex = "man";
}else{
r.name = "丽丽";
r.sex = "女";
}
x = (x+1)%2;
}
}
}

class Output implements Runnable{
private Res r;
Output(){
this.r = r;
}
public void run(){
while(true){
System.out.println("nam:"+r.name+"..."+"sex:"+r.sex);
}
}
}

class InputOutputDemo{
public static void main(String[] args){
Res r = new Res();
Input in = new Input(r);
Output out = new Output(r);

Thread t1 = new Thread(in);
Thread t2 = new Thread(out);

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

运行上面的程序会发现有安全性问题。
对于这种多个线程操作同一个资源,但是不是同一个操作,该如何同步呢?
解决方法:

class Output implements Runnable{
private Res r;
Output(){
this.r = r;
}
public void run(){
while(true){
synchorized(r/Input.class/Output.class/Res.class){
System.out.println("nam:"+r.name+"..."+"sex:"+r.sex);
}
}
}
}

class Input implements Runnable{
private Res r;
Input(Res r)
{
this.r = r;
}
public void run(){
int x = 0;
while(true){
synchorized(r/Input.class/Output.class/Res.class){
if(x==0){
r.name = "mike";
r.sex = "man";
}else{
r.name = "丽丽";
r.sex = "女";
}
x = (x+1)%2;
}
}
}
}

五、守护线程

在Java中有两类线程:用户线程 (User Thread)、守护线程 (Daemon Thread)。

所谓守护 线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。

用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。

将线程转换为守护线程可以通过调用Thread对象的setDaemon(true)方法来实现。在使用守护线程时需要注意一下几点:

(1) thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
(2) 在Daemon线程中产生的新线程也是Daemon的。
(3) 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断

import java.util.concurrent.TimeUnit;

/**

* 守护线程

*/


public class Daemons {
/**

* @param args

* @throws InterruptedException

*/


public static void main(String[] args) throws InterruptedException {

Thread d = new Thread(new Daemon());

d.setDaemon(true); //必须在启动线程前调用

d.start();

System.out.println("d.isDaemon() = " + d.isDaemon() + ".");

TimeUnit.SECONDS.sleep(1);
}
}

class DaemonSpawn implements Runnable {

public void run() {
while (true) {
Thread.yield();
}
}
}

class Daemon implements Runnable {

private Thread[] t = new Thread[10];
public void run() {
for (int i=0; i<t.length; i++) {

t[i] = new Thread(new DaemonSpawn());

t[i].start();

System.out.println("DaemonSpawn " + i + " started.");

}

for (int i=0; i<t.length; i++) {

System.out.println("t[" + i + "].isDaemon() = " +
t[i].isDaemon() + ".");

}
while (true) {
Thread.yield();
}
}
}

运行结果:
d.isDaemon() = true.

DaemonSpawn 0 started.

DaemonSpawn 1 started.

DaemonSpawn 2 started.

DaemonSpawn 3 started.

DaemonSpawn 4 started.

DaemonSpawn 5 started.

DaemonSpawn 6 started.

DaemonSpawn 7 started.

DaemonSpawn 8 started.

DaemonSpawn 9 started.

t[0].isDaemon() = true.

t[1].isDaemon() = true.

t[2].isDaemon() = true.

t[3].isDaemon() = true.

t[4].isDaemon() = true.

t[5].isDaemon() = true.

t[6].isDaemon() = true.

t[7].isDaemon() = true.

t[8].isDaemon() = true.

t[9].isDaemon() = true.

六、join、优先级、yield

①join()方法
join() 的作用:让“主线程”等待“子线程”结束之后才能继续运行。这句话可能有点晦涩,我们还是通过例子去理解:

// 主线程
public class Father extends Thread {
public void run() {
Son s = new Son();
s.start();
s.join();
...
}
}
// 子线程
public class Son extends Thread {
public void run() {
...
}
}

说明:
上面的有两个类Father(主线程类)和Son(子线程类)。因为Son是在Father中创建并启动的,所以,Father是主线程类,Son是子线程类。
在Father主线程中,通过new Son()新建“子线程s”。接着通过s.start()启动“子线程s”,并且调用s.join()。在调用s.join()之后,Father主线程会一直等待,直到“子线程s”运行完毕;在“子线程s”运行完毕之后,Father主线程才能接着运行。 这也就是我们所说的“join()的作用,是让主线程会等待子线程结束之后才能继续运行”!

②yield()
暂停当前正在执行的线程对象,并执行其他线程。
Thread.yield()方法作用是:暂停当前正在执行的线程对象,并执行其他线程。
yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
注意:
yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。

class ThreadYield extends Thread{  
public ThreadYield(String name) {
super(name);
}

@Override
public void run() {
for (int i = 1; i <= 50; i++) {
System.out.println("" + this.getName() + "-----" + i);
// 当i为30时,该线程就会把CPU时间让掉,让其他或者自己的线程执行(也就是谁先抢到谁执行)
if (i ==30) {
this.yield();
}
}

}
}

public class Main {

public static void main(String[] args) {

ThreadYield yt1 = new ThreadYield("张三");
ThreadYield yt2 = new ThreadYield("李四");
yt1.start();
yt2.start();
}

}

运行结果:
第一种情况:李四(线程)当执行到30时会CPU时间让掉,这时张三(线程)抢到CPU时间并执行。

第二种情况:李四(线程)当执行到30时会CPU时间让掉,这时李四(线程)抢到CPU时间并执行。

sleep()和yield()的区别
sleep()和yield()的区别):sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。
sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程
另外,sleep 方法允许较低优先级的线程获得运行机会,但 yield() 方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I\O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。