进程的定义:一个正在执行的程序
每一个进程执行都有一个执行顺序。该顺序是一个执行路径或者叫一个控制单元,线程就是进程中的一个独立的控制单元,线程在控制着进程的执行,线程是进程的内容。每一个进程至少有一个线程。
多线程创建方式有两种:
第一种:
1、定义一个类继承Thread
2、复写Thread类中的run方法
3、调用线程start方法(该方法两个作用一是启动线程,二是调用run方法)
为什么要覆盖run方法?
Thread类用于描述线程,该类就定义了一个功能,用于存储线程要运行的代码,该存储就是run方法。
覆盖run方法的目的是:
将自定义的代码存储到run方法中去,让线程运行
获取线程以及名称的方法:
currendThread()获取当前线程对象
getName()获取线程名称
eg:
public class Test1 {
public static void main(String[] args) {
Thread2 t2=new Thread2();//创建线程对象
t2.start();//开启线程
Thread1 t1=new Thread1();
t1.start();
}
}
class Thread1 extends Thread{//继承Thread类并且覆盖run方法
public void run(){
for(int i=0;i<=50;i++){
System.out.println(Thread.currentThread().getName()+":"+this.getName()+":"+this.getPriority()+":"+this.getId()+":"+"线程1");
//输出当前线程名 当前线程名 当前线程优先级 当前线程标示符
}
}
}
/*
在上面的类中Thread.currentThread().getName()于this.getName()得到的结果是一致的,表明Thread.currentThread()是获取当前线程的对象引用
*/
class Thread2 extends Thread{
public void run(){
for(int i=0;i<=50;i++){
System.out.println(this.getName()+":"+this.getPriority()+":"+this.getId()+":"+"线程2");
}
}
}
第二种创建线程的方法:
1、定义类实现Runnable接口
2、覆盖Runnable接口的run方法
3、通过Thread类建立线程对象
4、将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数
(原因:自定义的run方法所属的对象时Runnable接口的子类对象,所以要让线程去指定对象的run方法,就必须明确该类的run方法所属对象)
5、调用Thread类的start方法,开启线程并调用Runnable接口类的run方法
对于这种方法让我想到了在学接口的时候遗留下来的问题对接口的扩展性不太明,但看看多线程的这种方法让我明白了接口是如何实现扩展性的
就好比下面的代码
public class Practice11 {
public static void main(String[] args) {
new Deal(new Mp3()).print();
new Deal(new Mp4()).print();
}
}
class Deal{//处理接口传来的信息
private Usb u;
Deal(Usb u){//获取接口对象
this.u=u;
}
void print(){
u.speak();
}
}
interface Usb{//定义一个Usb接口用来实现功能扩展
void speak();
}
class Mp3 implements Usb{//功能1
public void speak() {
System.out.println("我是mp3");
}
}
class Mp4 implements Usb{//功能2
public void speak() {
System.out.println("我是Mp4");
}
}
对于Usb当需要功能扩展的时候只需要再建一个功能类继承Usb接口就可以了其中Deal就相当于多线程中的Thread类而speak就相当于Runnable中的run方法print就相当于start方法通过前后的联系可以对知识更好的理解,现在对接口的扩展性也有了个初步的小成,还有这也用到了多态的知识,在接口传递信息的时候就用到了多态中的子类覆盖父类的方法来实现的。
通过上面对接口的理解下面就是实现线程:
public class Test2 {
public static void main(String[] args) {
new Thread(new RunThread1()).start();//启动线程
new Thread(new RunThread2()).start();
}
}
class RunThread1 implements Runnable//实现Runnable接口
{
public void run(){//覆盖Runnable的run方法
for(int i=0;i<20000000;i++){
System.out.println("线程1");
}
}
}
class RunThread2 implements Runnable
{
public void run(){
for(int i=0;i<=2000000;i++){
System.out.println("线程2");
}
}
}
通过看这个程序和上面的样式差不多,都是运用了接口的扩展性。
对于以上两种实现多线程的方式,他们的区别就是在于接口避免了单继承的局限性,在创建线程的时候建议使用实现的方式,还有继承将代码放在Thread类中的run方法里面
实现将代码放在Runnable接口类中的run方法中。
多线程的运行出现的安全问题:
当多条语句在操作同一个线程的共享数据的时候,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行,导致共享数据的错误。
对于这种安全问题的解决办法为:对多条操作共享数据的语句,只能让一个线程执行完后,再让另一个线程执行,在一个线程执行的期间其他线程不可参与执行。这就需要锁这个概念来解决,锁实现了同步代码块,它的格式为
synchronized(对象){
需要被同步的代码
}
格式中的对象如同锁,持有锁的线程可以在同步中执行,没有持锁的线程即使获得了cpu执行权也不能执行,因为他没有获得锁。
同步的前提:
1、必须要有两个或者两个以上的线程
2、必须是多线程使用同一个锁,必须保证同步中只有一个线程
如何找同步代码块:
1、明确哪些代码是多线程的代码
2、明确共享数据
3、明确多线程运行代码中哪些语句是操作共享数据的
分析售票系统总共有100张票,有多个窗口买票实现程序如下:
class Ticket implements Runnable{
int number=1000;//初始化票的数量
public void run(){//覆盖Runnable中的run方法
while(number>0)
synchronized(this)//同步代码块,处理共享数据
{
if(number<=0)//当票数为零时结束循环从而达到结束线程的目的
break;
number--;//number为共享数据
System.out.println(Thread.currentThread()+":"+number);//打印结果
}
}
}
死锁:
出现死锁就是因为线程相互之间争夺资源而产生的在程序中一般是同步中嵌套同步如下面代码
class DeathThread implements Runnable{
boolean flag=true;
Object obj1=new Object();//定义一号锁标记符
Object obj2=new Object();//定义二号锁标记符
public void run(){
if(flag){
while(true){
synchronized(obj1){
System.out.println("线程1");
synchronized(obj2){//将有obj2标记的锁嵌套到有obj1标记的锁中,已达到死锁的效果
System.out.println("线程1");//通过同步中嵌套同步来实现死锁的现象
}
}
}
}else{
while(true){
synchronized(obj2){
System.out.println("线程2");
synchronized(obj1){
System.out.println("线程2");
}
}
}
}
}
}
在实际开发过程中一定要避免死锁的发生
单利设计模式:
饿汉式:
class Single{
private Single(){
}
private static Single s=new Single();
public static Single getInstance(){
return s;
}
}
懒汉式:
class Single{
private static Single s;//定义为静态的是为了共享数据
private Single(){}
private static Single getInstance(){
if(s==null){//第一次判空是为了当对象生成后不需要判断锁,提高效率
synchronized(Single.class){
if(s==null)//这次判空是为了判断已经进入第一次判断的语句,是否为空
s=new Single();
}
return s;
}
}
对于懒汉式再编程中一般不会用用到,但是面试中会经常出现
对于懒汉式还有一种加锁方式就是在getInstance函数上加锁,但由于每次掉用都要加锁,所以不如上面的效率高
懒汉式和饿汉式的区别在于,懒汉式没有安全问题,不需要加锁,而饿汉式存在安全问题,应该加上相应的锁,还有懒汉式在加载的时候就创建了对象,而饿汉式是在调用的时候创建对象的