Java基础 浅解线程

线程:是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

进程是程序由开始到结束,按照设计好的程序步骤执行,而线程却不然可以并发执行,比如现在的cup都是多核多线程的,方便我们可以处理多个事情,而是单一的事情。

这个时候就会有两个概念,并发性和并行性。

并发:当有多个线程在操作时,如果系统只有一个CPU(而此cp是单核),则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。这种方式我们称之为并发。

并行:当系统有一个以上CPU(如果单核需要多个cpu,如果是多核的化,一个cpu也可以)时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行。

多核CPU可以同时执行多个进程。

而并发和并行两个看似一样,其实区别很大,并发是指在处理两个以上进程,我们在使用的时候感觉是在同时进行,其实不是而是cpu在通过时间间隔,相互切换而让使用者察觉不到。而并行就不是了因为现在很多cpu都是多核,可以同时运行两个以上的进行(当然不能超过多核的数量)。

所以微观上说,多核CPU可以同时执行多个进程,进程数与CPU核数相当。但宏观上说,由于CPU会分时间片执行多个进程,所以实际执行进程个数会远多于CPU核数。

顺便提一下,还有个协程,而协程一般出现再爬虫中,暂时不说,这一篇文章主要是写java 的线程。

线程一般有5个状态:创建线程,等待(也有说六种,其中又添加超时等待),运行线程,线程阻塞,线程结束

java 中一般线程的创建有三种方式,继承Thread类,实现Runnable 接口和通过callable来创建线程。

一般基础的使用的前两种,而第三种一般时候不会讲解,就算讲解也是简单提一下,不过工作中会用。

继承Thread

public class Test extends Thread{
	@Override
	public void run() {
		// TODO Auto-generated method stub
	}
g
}

在使用的时候,一般我们都会重写run方法,而线程的调用,不是通过对象调用run方法,而是通过start方法如下:

public class Test extends Thread{
	public static void main(String[] args) {
		Test test=new Test();
		test.start();
	}
	@Override
	public void run() {
		System.out.println("线程一个");
	}

}
//输出
线程一个

线程类和普通的类一样可以有自己的构造方法,以及属性。当然是 有需要的时候可以添加。

实现Runable接口

class sonThread implements Runnable{

	@Override
	public void run() {
		// TODO Auto-generated method stub
		
	}
}

实现runnable接口如上,但是其调用又与继承thread类有些不同

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

class MyThread implements Runnable{

	@Override
	public void run() {
	 System.out.println("实现runnable接口的线程");	
	}
	
}
//输出
实现runnable接口的线程

有时候新jdk中调用会用lambda方式进行调用,如果不太了解,可以看我以前的文章 java基础 浅解1.8新增lambda表达式

Callable

class MyThread implements Callable<String>{//callable 可以返回值,所以泛型根据自己来定义,目前用String

	@Override
	public String call() throws Exception {
		// TODO Auto-generated method stub
		return null;//因为上面泛型是String,所以这个返回String
	}
}

Callable调用线程又有不同,调用如下:

public class Test {
	public static void main(String[] args) {
		
		try {
			FutureTask<String> task=new FutureTask<>(new MyThread());
			Thread my=new Thread(task, "test");
			my.start();			
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}
}
// 上面的每次需要新建一个线程,



//下面这个是常用的一种方式,直接用线程池,进行的一个调用,一般格式如下:
public class Test {
	public static void main(String[] args) {
		
		try {
//		 可以新建一个同时启动10个线程,
			ExecutorService  service=Executors.newFixedThreadPool(10);
//		 提交执行任务
			Future<String> task=service.submit(new MyThread());
//			得到返回值
			String str=task.get();
//			节约资源,关闭服务
			service.shutdown();		
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}
}

在调用Callable的线程时,用的java类其实用的时java中的juc中的类。如果有必要可以单独再写一篇文章进行浅解。

线程给予了我们最大可能利用电脑的性能之外,还造成了一个新的问题,就是数据的不安全性。比如银行卡提钱的功能,同时两人对一个卡的钱进行提款,当然在两个不同的银行。

class MyThread implements Runnable{
	int moneyCount=500;
	@Override
	public void run() {
	 if (moneyCount-300>0) {
		 try {
			Thread.sleep(10);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		 moneyCount=moneyCount-300;
		 System.out.println(Thread.currentThread().getName()+"拿了300,账户里面还有"+moneyCount);
		 
	 }
	}
	
}



public class Test {
	public static void main(String[] args) {
		Runnable mythread=new MyThread();
		new Thread(mythread,"丈夫").start();
		new Thread(mythread,"老婆").start();
			}
}
//输出
老婆拿了300,账户里面还有-100
丈夫拿了300,账户里面还有-100

看到这个结果,惊不惊喜意不意外,当然如果没有sleep的话,可能不会输出两行,但是程序游行方法的运行,应该会超过10毫秒的,所以这个只是塑造了一个假想,不要过度思考即可。

这个时候,应该有疑问了,为什么不用thread类而使用Runnable这个接口。这个改成thread是否可行。先不解释,还是继续上代码;

class MyThread extends Thread{
	int moneyCount=500;
	@Override
	public void run() {
	 if (moneyCount-300>0) {
		 try {
			Thread.sleep(10);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		 moneyCount=moneyCount-300;
		 System.out.println(Thread.currentThread().getName()+"拿了300,账户里面还有"+moneyCount);
		 
	 }
	}
	
}


public class Test {
	public static void main(String[] args) {
		MyThread mythread=new MyThread();
		
		mythread.start();
		mythread.start();
			}
}
//输出
Exception in thread "main" java.lang.IllegalThreadStateException
	at java.lang.Thread.start(Unknown Source)
	at test.Test.main(Test.java:14)
Thread-0拿了300,账户里面还有200

编译无问题,但是运行就报错了,为什么因为继承thread类的线程,只能运行一次start。

runable接口和thread类的一个区别,就是runable创建的线程,可以操作同一个对象,而thread创建的线程,却无法共同操作一个对象。

这个说明一点,如果类和对象有区别的,同一个类,只有通过new创建一次就是一个新对象。不要把对象类和对象搞混乱了。多嘴说一下 thread类其实也是继承了runable。所以用一个非官方的方式来调用也是可以的

class MyThread extends Thread{
	int moneyCount=500;
	@Override
	public void run() {
	 if (moneyCount-300>0) {
		 try {
			Thread.sleep(10);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		 moneyCount=moneyCount-300;
		 System.out.println(Thread.currentThread().getName()+"拿了300,账户里面还有"+moneyCount);
		 
	 }
	}
	
}


public class Test {
	public static void main(String[] args) {
		MyThread mythread=new MyThread();
		new Thread(mythread,"丈夫").start();
		new Thread(mythread,"老婆").start();
			}
}

//输出
老婆拿了300,账户里面还有-100
丈夫拿了300,账户里面还有-100

而这个调用方式可行,但是一般继承了thread类,然后进行调用多线程的时候,不会这样调用,因为在jdk帮助文档中,继承thread类中的调用时没有这样使用的。所以知道即可,暂不深究,所以按照传统的继承thread类的方式进行多线程调用即可,所以理解为,继承thread类不可操作同一对象。

而在一般使用多线程的时候,常用的是runnable接口实现因为有以下有点或者说与继承thread类实现多线程的区别。

  • Runnable的优点
    • 使用runable接口实现线程,可以继承多个接口,而thread类只能继承一个.
    • runnable可以共享一个对象,而thread却无法相互独立无法共享资源

上面说了这样多,可以多线程的弊端就是其无法保证数据的安全,java未来保证其数据的安全,在多线程中有synchronized关键字和lock类,保证数据的安全.

简单提一些几个个线程中常用的两个方法,一个是sleep.也就是让本线程暂时休眠一下需要写时间.

还有一个就是礼让yield,不过这个礼让是看心情,就像在公交车上碰见一个健壮的老人,可以让也可以不让.

还有一个重要的方式,就是强制插队,也就是类似的vip,我来了就需要我先办理事情,其他人后面等着.也就是join方法

class MyThread implements Runnable{

	public  void run() {
		for (int i=0;i<10;i++) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+"的线程循环"+i);
		}
		
	}
}

public class Test {

	public static void main(String[] args) {
		
		Runnable mythread=new MyThread();
		Thread myt=new Thread(mythread,"runnable");
		myt.start();
		
		for (int i=0;i<10;i++) {
			try {
				myt.join();
			} catch (InterruptedException e1) {
				// TODO Auto-generated catch block
				e1.printStackTrace();
			}
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			System.out.println("main"+i);
		}
    }
	}

//输出
runnable的线程循环0
runnable的线程循环1
runnable的线程循环2
runnable的线程循环3
runnable的线程循环4
runnable的线程循环5
runnable的线程循环6
runnable的线程循环7
runnable的线程循环8
runnable的线程循环9
main0
main1
main2
main3
main4
main5
main6
main7
main8
main9


虽然线程中运行的sleep时间, 比main中要长,单是输出的结果就是main需要等线程运行完,才会继续运行主线程,如果有疑问可以将join()注释掉看结果.

既然有强制插队,所以线程应该也有自己的权重,也就是自己优先级,

public class Test {
	public static void main(String[] args) {
		System.out.println(Thread.currentThread().getPriority());
		Runnable runnale=()->{System.out.println(Thread.currentThread().getPriority());};
		new Thread(runnale).start();
    }
}
//输出
5
5
    
    
public class Test {
	public static void main(String[] args) {
		System.out.println(Thread.currentThread().getPriority());
		Runnable runnale=()->{System.out.println(Thread.currentThread().getPriority());};
		Thread mythread=new Thread(runnale);
		mythread.setPriority(Thread.MAX_PRIORITY);
		mythread.start();
		
		Thread mythread1=new Thread(runnale);
		mythread1.setPriority(Thread.MIN_PRIORITY);
		mythread1.start();
    }
}
//输出
5
10
1    

可以看出其优先级默认都是5,优先级的默认范围是1–10;

现在好奇了优先级有什么用呢?

public class Test {
	
	public static void main(String[] args) {
		System.out.println(Thread.currentThread().getPriority());
	    Runnable runnable=new MyThread();
	    Thread mythread=new Thread(runnable,"a");
	    mythread.start();
	    
	    Thread mythread1=new Thread(runnable,"b");
	    mythread1.setPriority(8);
	    mythread1.start();
    }
}

class MyThread implements Runnable{
	public  void run() {
		for (int i=0;i<1000;i++) {
			System.out.println(Thread.currentThread().getName()+"的线程循环"+i);
		}
    }

​ 其中mythread1.setPriority(8),这个如果不存在的话,运行a,b两个线程结束的话都有先后,如果添加了mythread1.setPriority(8)那么b线程的结束概率要高于a.

  • 注意优先级
    • 第一,优先级是java虚拟机对线程资源的一个优先权,但不是绝对一定是先.
    • 第二:因为cpu运行的相互交替,而执行优先级也会执行优先级低的.

synchronized关键字

synchronized是为一个同步的方法或方法块进行加锁,在其处理完事务之后再自动释放.但是两者都是锁定的是当前对象.

  • 注意,synchronized可以修饰方法,修饰代码块,但是不能修饰构造器、成员变量等。

所以在使用中请注意.

同步方法

同步方法,格式就是在其多线程下操作会出错的方法中加入synchronized关键字即可

class MyThread implements Runnable{
	int moneyCount=500;
	//synchronized 同步方法
	public synchronized void run() {
     System.out.println(Thread.currentThread().getName()+"来银行要取钱");
	 if (moneyCount-300>0) {
		 try {
			Thread.sleep(10);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		 moneyCount=moneyCount-300;
		 System.out.println(Thread.currentThread().getName()+"拿了300,账户里面还有"+moneyCount);
		 
	 }
	}

}

public class Test {
	public static void main(String[] args) {
	
		Runnable mythread=new MyThread();
		new Thread(mythread,"丈夫").start();
		new Thread(mythread,"老婆").start();
    }
}
//输出
//丈夫来银行要取钱
//丈夫拿了300,账户里面还有200
//老婆来银行要取钱


如果对于结果有疑问,可以调换老婆和丈夫的顺序,然后看输出结果,都会只有在前面的调用才会输出拿到钱.可以多此运行,防止有偶然情况.

同步代码块

格式如下:

synchronized(obj)
{
    //需要被同步的代码块
}

同步代码块一定要注意的是obj这个对象,而在此方法中操作的具体对象是谁,这个需要很明白,不然就会在同步代码块中出现错误.

class MyThread implements Runnable{
	int moneyCount=500;
	// 同步方法
	public  void run() {
        synchronized(this){
     System.out.println(Thread.currentThread().getName()+"来银行要取钱");
	 if (moneyCount-300>0) {
		 try {
			Thread.sleep(10);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		 moneyCount=moneyCount-300;
		 System.out.println(Thread.currentThread().getName()+"拿了300,账户里面还有"+moneyCount);
		 
	 }
	}
}
}

public class Test {
	public static void main(String[] args) {
	
		Runnable mythread=new MyThread();
		new Thread(mythread,"丈夫").start();
		new Thread(mythread,"老婆").start();
    }
}
//输出
//丈夫来银行要取钱
//丈夫拿了300,账户里面还有200
//老婆来银行要取钱

当然这个中的操作对象就是其本身,所以我们用this代替即可,如果怀疑输出结果的偶然性也可以调整顺序多次尝试.

记住无论什么对象调用锁,一定要注意synchronized只会为自己对象调用加锁,其他对象调用毫无作用.

class MyThread implements Runnable{
	int moneyCount=500;
	// 同步方法
	public  void run() {
        synchronized(this){
     System.out.println(Thread.currentThread().getName()+"来银行要取钱");
	 if (moneyCount-300>0) {
		 try {
			Thread.sleep(10);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		 moneyCount=moneyCount-300;
		 System.out.println(Thread.currentThread().getName()+"拿了300,账户里面还有"+moneyCount);
		 
	 }
	}
}
}

public class Test {
	public static void main(String[] args) {
	
		Runnable mythread=new MyThread();
		new Thread(mythread,"张三").start();
        new Thread(mythread,"张三老婆").start();
		Runnable mythread2=new MyThread();
		new Thread(mythread2,"李四").start();
        new Thread(mythread2,"李四老婆").start();
    }
}

//输出
张三来银行要取钱 //a1
李四来银行要取钱 //b1
张三拿了300,账户里面还有200 //a2
李四拿了300,账户里面还有200 //b2
张三老婆来银行要取钱 //a3
李四老婆来银行要取钱 //b3

可以看出两个对象,直接的同步代码块直接毫无影响,换程同步方法结果也是一样的(说a三条的循环,以b三条的循环,其中ab之间的前后可能会有差异),所以不要单独的看代码觉得调用了同一个类,应该相互制约,但是synchronized只会为调用的对象进行个安全加锁.

  • 死锁

当然在使用synchronized同步的时候,会有死锁现象,就是两个条件相互制约,但是同时需要对方打开.比如两个人吃饭,但是只有一套碗筷,然后习惯不同,一人喜欢先拿碗,一个喜欢先拿筷,然后都在等对方使用完.


class Bowl {
	
	
}
class Chopsticks {
	
	
}

class EatFood implements Runnable{
	
	 static Bowl bowl =new Bowl() ;//静态属性,也就是类属性,如有以为可以去看我写的关于对象的文章
	 static Chopsticks chopsticks  =new Chopsticks() ;
	 int peopleType;
	 String peopleName;
	 
	 public EatFood( int peopleType,String peopleName) {
		this.peopleType=peopleType;
		this.peopleName=peopleName;
	}

	@Override
	public void run() {
		try {
			this.eat();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	private  void eat() throws InterruptedException {
		if (this.peopleType==0) {
			//因为前提是筷子和碗都只有一个,所以两个都需要加一个锁
			synchronized(bowl) {
				System.out.println(this.peopleName+"拿到了碗,在等筷子");
					Thread.sleep(1000);
				synchronized(chopsticks) {
					System.out.println(this.peopleName+"拿到了筷子,吃饭了");
				}
			}
		}else {
			
			synchronized(chopsticks) {
				System.out.println(this.peopleName+"拿到了筷,在等子碗");

					Thread.sleep(1000);
				
				synchronized(bowl) {
					System.out.println(this.peopleName+"拿到了子碗,吃饭了");
				}
			}
			
		}
	}
}

public class Test {
	public static void main(String[] args) {
		Runnable eatFood0=new EatFood(0, "阿黄");
		new Thread(eatFood0).start();
		Runnable eatFood1=new EatFood(1, "阿红");
		new Thread(eatFood1).start();
    }
}
//输出
阿黄拿到了碗,在等筷子
阿红拿到了筷,在等子碗

因为都在等彼此的资源,然而都没有释放,所以进入死锁状态.

上面我们一直提出,synchronized同步代码块和同步方法,都说是加锁,那线程有没有一个lock的类或者关键字呢?的确有而且一般会出现一对那就是lock’和unlock.

当然lock本身就是一个接口,所以一般的时候我们使用实现这个接口的ReentrantLock类.

一般使用lock格式

lock.lock();//lock是实现lock接口的类new成的对象名
try{
    
} finally {
lock.unlock();
		}

具体实例如下:

class MyThread implements Runnable{
	int moneyCount=500;
	Lock lock=new ReentrantLock();
	public  void run() {
		lock.lock();
		try {
		System.out.println(Thread.currentThread().getName()+"来银行要取钱");
	 if (moneyCount-300>0) {
		 try {
			Thread.sleep(10);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		 moneyCount=moneyCount-300;
		 System.out.println(Thread.currentThread().getName()+"拿了300,账户里面还有"+moneyCount);
		 
	 }} finally {
			lock.unlock();
		}}
	
	

}

public class Test {
	public static void main(String[] args) {
	
		Runnable mythread=new MyThread();
		new Thread(mythread,"张三").start();
        new Thread(mythread,"张三老婆").start();

    }
}

//输出
张三来银行要取钱 
张三拿了300,账户里面还有200 
张三老婆来银行要取钱 

这个时候肯定有人想到了同步代码块中的京东案例,生产者和消费者的案例,而如果用synchronized关键字肯定不行的,所以这个就了一个wait、notify、notifyAll 三个方法.

因为synchronized可以阻止并发更新同一个共享资源,实现了同步,但是无法实现不同线程之间的信息消息传递(通信).

wait()或wait(long time) 让当前线程等待.

notify()唤醒当前对象的等待线程,如果在当前线程中有多个等待的就随机唤醒一个.

notifuAl()唤醒当前对象线程的所有等待的线程.当然优先级的线程会优先调度.

注意:上面四个方法都是object的方法,都只能在同步方法或者同步代码块中使用,否则就会抛出异常.>

那下面我们就开始实现生产者和消费者的案例,通过synchronized关键字进行实现.

一般消费者和生产者这个案例一般会分成三个部分:

生产者:生产者就是生产资源的类

消费者:消费者就是消耗资源的类

缓存区:无论如何生产者和消费者必须有一个一个部分,因为生产者和消费的交互或者说其有关系的部分.同样synchronized同步必须同同一个对象,因此这个是其必须拥有的部分.

具体看代码,以下代码黏贴可以用

class SysContain{
	Chicken[] chickens=new Chicken[10];
	int count=0;
	public synchronized void push(Chicken chicken) {
//		盘对事发后容器满了,西游等待消费者消费
		if (count==chickens.length) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		chickens[count]=chicken;
		count++;
		this.notify();
	}
	public synchronized Chicken pop() {
		if(count==0) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		count--;
		Chicken chicken=chickens[count];
		this.notify();
		return chicken;
	}
	
	
}

class Customer extends Thread{
	SysContain contain;
	public Customer(SysContain contain) {
		this.contain=contain;
	}
	public void run() {
		for (int i=0;i<100;i++) {
			System.out.println("========消费了id为"+contain.pop().id+"的鸡");
//			contain。push
		}
	}
	
}

class Producter extends Thread{
	

	SysContain contain;
	public Producter(SysContain contain) {
		this.contain=contain;
	}
	public void run() {
		for (int i=0;i<100;i++) {
			System.out.println("生产了id为"+i+"只鸡");
			contain.push(new Chicken(i));
		}
	}
	

	
}
class Chicken{
	int id;
	public Chicken(int id) {
		this.id=id;
	}
}
public class Test {
	public static void main(String[] args) {
		SysContain contain=new SysContain();
		new Customer(contain).start();
		new Producter(contain).start();
    }
}


既然关键字synchronized和lock都可以实现同步效果,那么两者有什么区别呢?

  • Synchronized是关键字,而lock是一个接口。
  • Synchronized不需要用户手动释放同步锁,无论是Synchronized方法还是Synchronized代码块,指向完毕之后,系统会自动让线程释放对锁的占用(哪怕发生异常也是自动释放)。而lock侧必须需要用户去手动释放锁。如果不lock不手动释放错就会出现死锁的现象,所以一般unlock放在finally块中。
  • 通过lock可以指定有没有成功获取锁,以及在等待锁的线程响应中断,而synchronized却不行,其不但无法时钟释放成功上锁,以及中途中断(如果你要说重启电脑和服务器器的话不就中断了吗,这样想的话就当我没说)。
  • lock可以提高多个线程进行读操作的效率。 在性能上来说,如果竞争资源不激烈的话,两者性功是差别不大,但是当竞争激烈的时候,也就是大量线程同时竞争的话,lock的性能就要远远优于synchronized。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值